Access Control for Flask-Admin
Asked Answered
S

0

3

My flask app centers around modifying models based on SQLAlchemy. Hence, I find flask-admin a great plugin because it maps my SQLA models to forms with views already defined with a customizable interface that is tried and tested.

I understand that Flask-admin is intended to be a plugin for administrators managing their site's data. However, I don't see why I can't use FA as a framework for my users to CRUD their data as well.

To do this, I have written the following:

class AuthorizationRequiredView(BaseView):

  def get_roles(self):
    raise NotImplemented("Override AuthorizationRequiredView.get_roles not set.")

  def is_accessible(self):
    if not is_authenticated():
      return False
    if not current_user.has_role(*self.get_roles()):
      return False
    return True

  def inaccessible_callback(self, name, **kwargs):
    if not is_authenticated():
      return current_app.user_manager.unauthenticated_view_function()
    if not current_user.has_role(*self.get_roles()):
      return current_app.user_manager.unauthorized_view_function()


class InstructionModelView(DefaultModelView, AuthorizationRequiredView):

  def get_roles(self):
    return ["admin", "member"]

  def get_query(self):
    """Jails the user to only see their instructions.
    """
    base = super(InstructionModelView, self).get_query()
    if current_user.has_role('admin'):
      return base
    else:
      return base.filter(Instruction.user_id == current_user.id)

  @expose('/edit/', methods=('GET', 'POST'))
  def edit_view(self):
    if not current_user.has_role('admin'):
      instruction_id = request.args.get('id', None)
      if instruction_id:
        m = self.get_one(instruction_id)
        if m.user_id != current_user.id:
          return current_app.user_manager.unauthorized_view_function()
    return super(InstructionModelView, self).edit_view()

  @expose('/delete/', methods=('POST',))
  def delete_view(self):
    return_url = get_redirect_target() or self.get_url('.index_view')

    if not self.can_delete:
      return redirect(return_url)

    form = self.delete_form()

    if self.validate_form(form):
      # id is InputRequired()
      id = form.id.data

      model = self.get_one(id)

      if model is None:
        flash(gettext('Record does not exist.'), 'error')
        return redirect(return_url)

      # message is flashed from within delete_model if it fails
      if self.delete_model(model):

        if not current_user.has_role('admin') \
            and model.user_id != current_user.id:
          # Denial: NOT admin AND NOT user_id match
          return current_app.user_manager.unauthorized_view_function()

        flash(gettext('Record was successfully deleted.'), 'success')
        return redirect(return_url)
    else:
      flash_errors(form, message='Failed to delete record. %(error)s')

    return redirect(return_url)

Note: I am using Flask-User which is built on top of Flask-Login.

The code above works. However, it is difficult to abstract as a base class for other models which I would like to implement access control for CRUD operations and Index/Edit/Detail/Delete views.

Mainly, the problems are:

  1. the API method, is_accessible, does not provide the primary key of the model. This key is needed because in almost all cases relationships between users and entities are almost always stored via relationships or in the model table directly (i.e. having user_id in your model table).

  2. some views, such as delete_view, do not provide the instance id that can be retrieve easily. In delete_view, I had to copy the entire function just to add one extra line to check if it belongs to the right user.

Surely someone has thought about these problems.

How should I go about rewriting this to something that is more DRY and maintainable?

Schuss answered 24/10, 2017 at 15:9 Comment(1)
I would only use is_accessible the way you are using it in your example code. It is supposed to limit access to a specific view, not more than that. Then, every view, such as delete_view, has a hook that is easy to override. It would be either the delete_model function or can_delete decorated with @property in your example.Mummy

© 2022 - 2024 — McMap. All rights reserved.