backoffice: allow limited access to cards via dispatched functions (#66784) #615

Merged
fpeters merged 1 commits from wip/66784-card-limited-access-listing into main 2023-09-04 16:51:34 +02:00
11 changed files with 52 additions and 14 deletions

View File

@ -173,6 +173,32 @@ def test_carddata_management(pub):
resp = resp.form.submit('cancel')
assert resp.location.endswith('/backoffice/data/foo/')
# check access to a single card
carddef.workflow_roles = {'_editor': None}
carddef.backoffice_submission_roles = []
carddef.store()
assert app.get('/backoffice/data/', status=403)
assert app.get('/backoffice/data/foo/', status=403)
assert app.get(f'/backoffice/data/foo/{carddata.id}/', status=403)
carddata = carddef.data_class()()
carddata.just_created()
carddata.store()
assert app.get(f'/backoffice/data/foo/{carddata.id}/', status=403)
# simulate dynamic dispatch
carddata.workflow_roles = {'_editor': user.roles}
carddata.store()
assert app.get(f'/backoffice/data/foo/{carddata.id}/', status=200)
assert app.get('/backoffice/data/foo/', status=200)
assert app.get('/backoffice/data/', status=403)
# attach carddata to user (should not give access)
carddata.workflow_roles = {}
carddata.user_id = user.id
carddata.store()
assert app.get(f'/backoffice/data/foo/{carddata.id}/', status=403)
def test_carddata_management_categories(pub):
user = create_user(pub)

View File

@ -1765,7 +1765,7 @@ class FormsDirectory(AccessControlled, Directory):
get_response().set_backoffice_section(self.section)
return super()._q_traverse(path)
def is_accessible(self, user):
def is_accessible(self, user, traversal=False):
if is_global_accessible(self.section):
return True

View File

@ -1928,7 +1928,7 @@ class WorkflowsDirectory(Directory):
get_response().set_backoffice_section('workflows')
return super()._q_traverse(path)
def is_accessible(self, user):
def is_accessible(self, user, traversal=False):
if is_global_accessible():
return True

View File

@ -77,7 +77,9 @@ class DataManagementDirectory(ManagementDirectory):
def add_breadcrumb(self):
get_response().breadcrumb.append(('data/', _('Cards')))
def is_accessible(self, user):
def is_accessible(self, user, traversal=False):
if traversal:
return super().is_accessible(user=user, traversal=traversal)
Review

Quand on est en mode "traversal" (voir plus bas), on court-circuite pour envoyer sur la méthode parente qui se contentera de vérifier que l'accès backoffice est ok.

Quand on est en mode "traversal" (voir plus bas), on court-circuite pour envoyer sur la méthode parente qui se contentera de vérifier que l'accès backoffice est ok.
if not user.can_go_in_backoffice():
return False
if get_publisher().get_backoffice_root().is_global_accessible('cards') and CardDef.keys():
@ -111,6 +113,8 @@ class DataManagementDirectory(ManagementDirectory):
get_response().set_title(_('Cards'))
if not (CardDef.exists()):
return self.empty_site_message(_('Cards'))
if not self.is_accessible(get_request().user, traversal=False):
raise errors.AccessForbiddenError()
Review

Vérification ajoutée sur la page d'index pour lever une erreur si l'utilisateur n'avait pas accès (puisque lors de la traversée tous les cas n'auront pas été attrapés).

Vérification ajoutée sur la page d'index pour lever une erreur si l'utilisateur n'avait pas accès (puisque lors de la traversée tous les cas n'auront pas été attrapés).
return template.QommonTemplateResponse(
templates=['wcs/backoffice/data-management.html'], context={'view': self}
)

View File

@ -116,7 +116,7 @@ class JournalDirectory(Directory):
templates=['wcs/backoffice/journal.html'], context=context, is_django_native=True
)
def is_accessible(self, user):
def is_accessible(self, user, traversal=False):
return user.can_go_in_admin()
def get_filter_form(self):

View File

@ -168,7 +168,7 @@ class ManagementDirectory(Directory):
def add_breadcrumb(self):
get_response().breadcrumb.append(('management/', _('Management')))
def is_accessible(self, user):
def is_accessible(self, user, traversal=False):
return user.can_go_in_backoffice()
def _q_traverse(self, path):

View File

@ -88,7 +88,7 @@ class RootDirectory(AccessControlled, Directory):
return super()._q_traverse(path)
@classmethod
def is_accessible(cls, subdirectory):
def is_accessible(cls, subdirectory, traversal=False):
# check a backoffice directory is accessible to the current user
if getattr(get_response(), 'filter', {}) and get_response().filter.get('admin_for_all'):
@ -104,7 +104,7 @@ class RootDirectory(AccessControlled, Directory):
# if the directory defines a is_accessible method, use it.
if hasattr(getattr(cls, subdirectory, None), 'is_accessible'):
return getattr(cls, subdirectory).is_accessible(get_request().user)
return getattr(cls, subdirectory).is_accessible(get_request().user, traversal=traversal)
return cls.is_global_accessible(subdirectory)
@ -182,7 +182,7 @@ class RootDirectory(AccessControlled, Directory):
def _q_index(self):
for directory in ('studio', 'management'):
if self.is_accessible(directory):
if self.is_accessible(directory, traversal=True):
Review

Pigé la PR dans son ensemble, sauf ici, pas compris en quoi traversal=True ajouté ici n’est pas trop permissif ?

Pigé la PR dans son ensemble, sauf ici, pas compris en quoi `traversal=True` ajouté ici n’est pas trop permissif ?
Review

c'est très permissif mais justement derrière les vues en elles-mêmes font le contrôle et donc c'est ok si on laisse passer ici.

c'est très permissif mais justement derrière les vues en elles-mêmes font le contrôle et donc c'est ok si on laisse passer ici.
return redirect(directory + '/')
raise errors.AccessForbiddenError()
@ -198,7 +198,9 @@ class RootDirectory(AccessControlled, Directory):
def _q_lookup(self, component):
if component in [str(x[0]).strip('/') for x in self.menu_items]:
if not self.is_accessible(component):
if not self.is_accessible(component, traversal=True):
# traversal=True will make it skip some expensive checks and
# let directories/views further down apply their own checks.
Review

Ajout d'un paramètre traversal aux méthodes is_accessible(), qui permet d'indiquer quand l'accès se fait juste pour la traversée d'URL, et qu'un contrôle d'accès plus poussé aura lieu plus loin.

Ajout d'un paramètre traversal aux méthodes is_accessible(), qui permet d'indiquer quand l'accès se fait juste pour la traversée d'URL, et qu'un contrôle d'accès plus poussé aura lieu plus loin.
raise errors.AccessForbiddenError()
return getattr(self, component)
return super()._q_lookup(component)
@ -218,7 +220,7 @@ class RootDirectory(AccessControlled, Directory):
display_function = options.get('check_display_function')
if display_function and not display_function(slug):
continue
if not self.is_accessible(slug):
if not self.is_accessible(slug, traversal=False):
continue
if callable(v):
label = v()

View File

@ -74,7 +74,7 @@ class ChangesDirectory(Directory):
},
)
def is_accessible(self, user):
def is_accessible(self, user, traversal=False):
return user.is_admin
@ -138,7 +138,7 @@ class StudioDirectory(Directory):
templates=['wcs/backoffice/studio.html'], context=context, is_django_native=True
)
def is_accessible(self, user):
def is_accessible(self, user, traversal=False):
backoffice_root = get_publisher().get_backoffice_root()
return (
backoffice_root.is_accessible('forms')

View File

@ -408,7 +408,7 @@ class SubmissionDirectory(Directory):
get_response().set_backoffice_section('submission')
return super()._q_traverse(path)
def is_accessible(self, user):
def is_accessible(self, user, traversal=False):
if not user.can_go_in_backoffice():
return False
# check user has at least one role set for backoffice submission

View File

@ -50,6 +50,9 @@ class CardDef(FormDef):
confirmation = False
# users are not allowed to access carddata where they're submitter.
user_allowed_to_access_own_data = False
category_class = CardDefCategory
def data_class(self, mode=None):

View File

@ -184,6 +184,9 @@ class FormDef(StorableObject):
# prefixes for formdata variables
var_prefixes = ['form']
# users are allowed to access formdata where they're submitter.
user_allowed_to_access_own_data = True
# declarations for serialization
TEXT_ATTRIBUTES = [
'name',
@ -1731,7 +1734,7 @@ class FormDef(StorableObject):
user_roles = ensure_role_are_strings(user_roles)
if formdata and formdata.is_submitter(user):
if self.user_allowed_to_access_own_data and formdata and formdata.is_submitter(user):
Review

Comme tous les agents peuvent désormais traverser jusque /cards/foo/id il faut cette modification pour qu'un agent ne puisse pas voir une fiche qui serait liée à son compte.

Comme tous les agents peuvent désormais traverser jusque /cards/foo/id il faut cette modification pour qu'un agent ne puisse pas voir une fiche qui serait liée à son compte.
return True
if self.is_of_concern_for_user(user):
if not formdata: