From 8d824f1fac3493ffde23e0fea8f6415516598942 Mon Sep 17 00:00:00 2001 From: Brian Peterson Date: Wed, 21 Aug 2019 15:09:08 -0600 Subject: [PATCH] Don't show role_only_columns if not role; allow viewing view if role in roles_accepted --- flask_secure_admin/__init__.py | 3 +- flask_secure_admin/base.py | 25 ++++++++++- flask_secure_admin/security/__init__.py | 4 ++ flask_secure_admin/security/model_view.py | 15 ++++++- .../security/role_scaffolding.py | 41 +++++++++++++++++++ 5 files changed, 83 insertions(+), 5 deletions(-) create mode 100644 flask_secure_admin/security/role_scaffolding.py diff --git a/flask_secure_admin/__init__.py b/flask_secure_admin/__init__.py index dca1d84..85118a8 100644 --- a/flask_secure_admin/__init__.py +++ b/flask_secure_admin/__init__.py @@ -1,4 +1,5 @@ from .base import SecureAdminBlueprint from .security import (SUPER_ROLE, SecureRedirectIndex, SecureDefaultIndex, - SecureModelView) + SecureModelView, scaffold_form_respecting_roles, + scaffold_list_columns_respecting_roles) diff --git a/flask_secure_admin/base.py b/flask_secure_admin/base.py index 560bd97..d229862 100644 --- a/flask_secure_admin/base.py +++ b/flask_secure_admin/base.py @@ -7,7 +7,11 @@ from flask_admin import helpers as admin_helpers, AdminIndexView, expose from flask_security import Security, login_required -from .security import SecureModelView, SecureDefaultIndex +from .security import ( + SecureModelView, SecureDefaultIndex, + scaffold_list_columns_respecting_roles, + scaffold_form_respecting_roles, SUPER_ROLE +) from .contrib.sqlsoup import override___name___on_sqlsoup_model, SQLSoupUserDataStore from .templates import load_master_template from .utils import encrypt_password, create_initial_admin_user @@ -42,7 +46,7 @@ class SecureAdminBlueprint(Blueprint): ] def __init__(self, name=None, models=None, - view_options=None, index_url=None, + view_options=None, admin_roles_accepted=None, *args, **kwargs): self.app_name = name assert self.app_name, "Admin instances must have a name value" @@ -50,6 +54,7 @@ def __init__(self, name=None, models=None, self.models.extend(self.DEFAULT_MODELS) self.view_options = view_options or [] self.view_options.extend(self.DEFAULT_VIEW_OPTIONS) + self.admin_roles_accepted = admin_roles_accepted or [SUPER_ROLE] # Initialize the below as a best practice, # so they can be referenced before assignment @@ -122,10 +127,26 @@ def on_after_add_layout_to_admin(self, admin, app, db, options): def add_layout_to_admin(self, admin, app, db, options): """ Add auth views, and for each additional model specified get the model from the database which must be set on app. """ + for model_name, view_options_bag in zip_longest( self.models, self.view_options, fillvalue={}): + + # Add default view options + if not view_options_bag.get('scaffold_list_columns'): + view_options_bag['scaffold_list_columns'] = \ + scaffold_list_columns_respecting_roles + if not view_options_bag.get('scaffold_form'): + view_options_bag['scaffold_form'] = \ + scaffold_form_respecting_roles + if not view_options_bag.get('role_only_columns'): + view_options_bag['role_only_columns'] = dict() + if not view_options_bag.get('roles_accepted'): + view_options_bag['roles_accepted'] = self.admin_roles_accepted + + # TODO: SQLSoup-specific model = getattr(db, model_name) model = override___name___on_sqlsoup_model(model) + DerviedModelViewCls = \ type(f'Secure{model.__name__}View', (SecureModelView,), diff --git a/flask_secure_admin/security/__init__.py b/flask_secure_admin/security/__init__.py index 953bf5c..52c0240 100644 --- a/flask_secure_admin/security/__init__.py +++ b/flask_secure_admin/security/__init__.py @@ -2,3 +2,7 @@ from .data import SUPER_ROLE from .model_view import SecureModelView from .indexes import SecureRedirectIndex, SecureDefaultIndex +from .role_scaffolding import ( + scaffold_list_columns_respecting_roles, + scaffold_form_respecting_roles +) diff --git a/flask_secure_admin/security/model_view.py b/flask_secure_admin/security/model_view.py index aa1966d..fa5e7c1 100644 --- a/flask_secure_admin/security/model_view.py +++ b/flask_secure_admin/security/model_view.py @@ -11,11 +11,22 @@ class SecureModelView(sqla.ModelView): def __repr__(self): return f"<'{self.name}' ModelView>" + def rebuild_views_respecting_access(self): + # Rebuild edit & list views based on who is accessing them + self._refresh_forms_cache() + self._list_columns = self.get_list_columns() + + def has_one_accepted_role(self, user): + return any( + [current_user.has_role(r) + for r in self.roles_accepted]) + def is_accessible(self): if (current_user.is_active and current_user.is_authenticated and - current_user.has_role(SUPER_ROLE)): - return True + self.has_one_accepted_role(current_user)): + self.rebuild_views_respecting_access() + return True else: user_ref = 'AnonymousUser' if \ current_user.is_anonymous else \ diff --git a/flask_secure_admin/security/role_scaffolding.py b/flask_secure_admin/security/role_scaffolding.py new file mode 100644 index 0000000..3cdbdce --- /dev/null +++ b/flask_secure_admin/security/role_scaffolding.py @@ -0,0 +1,41 @@ + +from flask_admin.contrib.sqla.view import ModelView as SQLAModelView +from .data import SUPER_ROLE +from flask_security import current_user +from flask_admin.contrib.sqla.form import get_form as get_sqla_form + + +def scaffold_list_columns_respecting_roles(self): + """ Respect a new view option, `role_only_columns`, + in the list view. Must be a dictionary mapping + between role names and columns which only users + with this role are allowed to see. """ + columns = SQLAModelView.scaffold_list_columns(self) + role_only_columns = self.role_only_columns or dict() + super_only_columns = role_only_columns.get(SUPER_ROLE) or [] + if current_user and not current_user.has_role(SUPER_ROLE): + columns = [c for c in columns if c not in super_only_columns] + return columns + + +def scaffold_form_respecting_roles(self): + """ Just like regular SQLAModelView `scaffold_form()`, + except that we exclude `role_only_columns` + if user does not have the expected role. """ + exclude = list(self.form_excluded_columns or []) + role_only_columns = self.role_only_columns or dict() + super_only_columns = role_only_columns.get(SUPER_ROLE) or [] + if current_user and not current_user.has_role(SUPER_ROLE): + exclude.extend(role_only_columns) + converter = self.model_form_converter(self.session, self) + form_class = get_sqla_form(self.model, converter, + base_class=self.form_base_class, + only=self.form_columns, + exclude=exclude, + field_args=self.form_args, + ignore_hidden=self.ignore_hidden, + extra_fields=self.form_extra_fields) + + if self.inline_models: + form_class = self.scaffold_inline_form_models(form_class) + return form_class