diff --git a/src/backoffice/views/assemblies.py b/src/backoffice/views/assemblies.py index b2ca54024d054b3679937bb6d482162fceadd89a..04f3360b4156a35c6cbc94a92db2ff7f72f5b46c 100644 --- a/src/backoffice/views/assemblies.py +++ b/src/backoffice/views/assemblies.py @@ -59,7 +59,7 @@ class CreateAssemblyView(ConferenceLoginRequiredMixin, CreateView): def dispatch(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: member = ConferenceMember.objects.get(conference=self.conference, user=request.user) - if (member.is_staff and member.has_perm('core.assembly_team')) or self.conference.is_open: + if (member.is_staff and member.has_perms('core.assembly_team')) or self.conference.is_open: return super().dispatch(request, *args, **kwargs) raise PermissionDenied diff --git a/src/backoffice/views/mixins.py b/src/backoffice/views/mixins.py index 4fae86fa37db9c33a4e01433dc8794ad3dda9f60..b47660a1d6a07c65f62a5f21ad5e198d2d696abc 100644 --- a/src/backoffice/views/mixins.py +++ b/src/backoffice/views/mixins.py @@ -19,6 +19,8 @@ class ConferenceRequiredMixin(PermissionRequiredMixin): _conferencemember: ConferenceMember | None = None permission_required = [] + require_all_permissions = True + require_staff = False def __init__(self, *args, **kwargs): self._conference = kwargs.pop('conference', None) @@ -66,14 +68,14 @@ class ConferenceRequiredMixin(PermissionRequiredMixin): @property def is_assembly_team(self): - return self.conferencemember.user.is_authenticated and self.conferencemember.has_staff_permission('core.assembly_team') + return self.conferencemember.user.is_authenticated and self.conferencemember.has_perms('core.assembly_team', require_staff=True) @property def is_channel_team(self): return ( self.conference.support_channels and self.conferencemember.user.is_authenticated - and self.conferencemember.has_staff_permission(self.conference, 'core.channel_team') + and self.conferencemember.has_perms('core.channel_team', require_staff=True) ) def dispatch(self, request, *args, **kwargs): @@ -107,11 +109,12 @@ class ConferenceRequiredMixin(PermissionRequiredMixin): 'has_sos': self.conferencemember is not None, 'has_assemblies': self.is_assembly_team, 'has_channel': self.is_channel_team, - 'has_pages': self.conferencemember.has_staff_permission('core.static_pages'), - 'has_map': self.conferencemember.has_staff_permission('core.map_edit'), - 'has_moderation': self.conferencemember.has_staff_permission('core.moderation'), - 'has_schedules': self.conferencemember.has_staff_permission('core.scheduleadmin'), - 'has_workadventure': settings.INTEGRATIONS_WORKADVENTURE and self.conferencemember.has_staff_permission('core.workadventure_admin'), + 'has_pages': self.conferencemember.has_perms('core.static_pages', require_staff=True), + 'has_map': self.conferencemember.has_perms('core.map_edit', require_staff=True), + 'has_moderation': self.conferencemember.has_perms('core.moderation', require_staff=True), + 'has_schedules': self.conferencemember.has_perms('core.scheduleadmin', require_staff=True), + 'has_workadventure': settings.INTEGRATIONS_WORKADVENTURE + and self.conferencemember.has_perms('core.workadventure_admin', require_staff=True), } ) else: @@ -130,12 +133,20 @@ class ConferenceRequiredMixin(PermissionRequiredMixin): return context def has_permission(self): - required = self.get_permission_required() - if not required: + perms = self.get_permission_required() + if not perms: return True cm = self.conferencemember - return cm.has_perms(required) if cm is not None else False + return ( + cm.has_perms( + *perms, + require_all=self.require_all_permissions, + require_staff=self.require_staff, + ) + if cm is not None + else False + ) class ConferenceLoginRequiredMixin(LoginRequiredMixin, ConferenceRequiredMixin): @@ -170,13 +181,13 @@ class AssemblyMixin(ConferenceLoginRequiredMixin): assembly = Assembly.objects.get(conference=self.conference, pk=self.request.resolver_match.kwargs.get(self.assembly_url_param)) # check if it's the assembly team - if self.conferencemember.has_staff_permission('assembly_team'): + if self.conferencemember.has_perms('assembly_team', require_staff=True): self._assembly_staff_access = True self._staff_access = self._staff_access or assembly.state_assembly != Assembly.State.NONE self._staff_mode = True # check if it's the channel team - if self.conferencemember.has_staff_permission('channel_team'): + if self.conferencemember.has_perms('channel_team', require_staff=True): self._channels_staff_access = True self._staff_access = self._staff_access or assembly.state_channel != Assembly.State.NONE self._staff_mode = True diff --git a/src/core/models/assemblies.py b/src/core/models/assemblies.py index 1ec7d59b9a9048444795f225b8cda60807e48789..0b41c019b0eb2800ac6f6aff8677aa169d774f7e 100644 --- a/src/core/models/assemblies.py +++ b/src/core/models/assemblies.py @@ -31,7 +31,7 @@ from core.validators import FileSizeValidator, ImageDimensionValidator class AssemblyManager(ConferenceManagerMixin['Assembly']): - staff_permissions = ['assembly_team'] + staff_permissions = ['core.assembly_team'] assembly_filter = 'self' def apply_public_filter(self, queryset: 'QuerySet[Assembly]', member: ConferenceMember | None = None) -> 'QuerySet[Assembly]': @@ -402,7 +402,7 @@ class Assembly(TaggedItemMixin, models.Model): if not user.is_authenticated: return False - if staff_can_manage and user.has_conference_staffpermission(self.conference, 'assembly_team', 'channel_team'): + if staff_can_manage and user.has_conference_staff_permission(self.conference, 'assembly_team', 'channel_team'): return True return self.members.filter(member=user, can_manage_assembly=True).exists() @@ -504,7 +504,7 @@ class Assembly(TaggedItemMixin, models.Model): class AssemblyLinkManager(ConferenceManagerMixin['AssemblyLink']): - staff_permissions = ['assembly_team'] + staff_permissions = ['core.assembly_team'] conference_filter = 'a__conference' def apply_public_filter(self, queryset: 'QuerySet[AssemblyLink]', member: ConferenceMember) -> 'QuerySet[AssemblyLink]': @@ -548,7 +548,7 @@ class AssemblyLink(models.Model): class AssemblyMemberManager(ConferenceManagerMixin['AssemblyMember']): - staff_permissions = ['assembly_team'] + staff_permissions = ['core.assembly_team'] conference_filter = 'assembly__conference' assembly_filter = 'assembly' diff --git a/src/core/models/base_managers.py b/src/core/models/base_managers.py index aa2ff05a5a2e27911d2baaf3814e7dc75b484f4c..7463848f84b1ff3534dacd28ad3dc1ee56973986 100644 --- a/src/core/models/base_managers.py +++ b/src/core/models/base_managers.py @@ -184,7 +184,7 @@ class ConferenceManagerMixin(models.Manager, Generic[_ModelType]): if not isinstance(member, ConferenceMember): return self.conference_accessible(conference=conference, member=member) if show_public else qs.none() if member.is_staff and staff_can_manage: - if self.staff_permissions is None or (self.staff_permissions and member.has_staff_permission(*self.staff_permissions)): + if self.staff_permissions is None or (self.staff_permissions and member.has_perms(*self.staff_permissions, require_staff=True)): return qs id_list = ( self.apply_assembly_rights_filter(qs, member, only_manageable=only_manageable) diff --git a/src/core/models/conference.py b/src/core/models/conference.py index f1d5bbe2abbf68f5255a0c6949fbb7cf364f62c1..e06c906f0036790fb484cfe13d44df785184c37f 100644 --- a/src/core/models/conference.py +++ b/src/core/models/conference.py @@ -120,7 +120,10 @@ class ConferenceMember(models.Model): self.user = PlatformUser.get_anonymous_user() if user is None else user self.roles = [] - def has_staff_permission(self, *perms, need_all=False) -> bool: + def has_perms( + self, + *perms, + ) -> bool: return False is_authenticated = False @@ -131,26 +134,22 @@ class ConferenceMember(models.Model): return AnonMember() - def has_staff_permission(self, *perms, need_all=False) -> bool: - """ - Check if the user is staff and has the given permission(s) in the context of this conference. - :param perms: Permissions to check for. - :param need_all: If True, all given permissions must be present, otherwise at least one. + def has_perms(self, *perms: str, require_all: bool = True, require_staff: bool = False) -> bool: + """Check if a user has a set of permissions. + + Args: + *perms (str): The list of permissions to check. + require_all (bool, optional): Defines, if all permission are required or just one. Defaults to True. + require_staff (bool, optional): If a staff status is also required. Defaults to False. - :return: True if the user is staff and has the given permission(s), False otherwise. + Returns: + bool: True if the user has all permissions. """ - if not self.is_staff: + if require_staff and not self.is_staff: return False - if need_all: - return all(self.has_perm(('core.' + perm) if '.' not in perm else perm) for perm in perms) - else: - return any(self.has_perm(('core.' + perm) if '.' not in perm else perm) for perm in perms) - - def has_perm(self, perm): - return perm in self.all_permissions - - def has_perms(self, perm_list): - return all(perm in self.all_permissions for perm in perm_list) + if require_all: + return all(perm in self.all_permissions for perm in perms) + return any(perm in self.all_permissions for perm in perms) def __str__(self): return f'{self.user.username}@{self.conference.slug}' diff --git a/src/core/models/events.py b/src/core/models/events.py index bc1439f10660859c8d9f5911f2003e29d0e558ae..b04f29c132a757c8a58047b5ff57119ee0d34067 100644 --- a/src/core/models/events.py +++ b/src/core/models/events.py @@ -60,7 +60,7 @@ class EventDurationField(models.DurationField): class EventManager(ConferenceManagerMixin['Event']): - staff_permissions = ['assembly_team', 'scheduleadmin'] + staff_permissions = ['core.assembly_team', 'core.scheduleadmin'] conference_filter = 'assembly__conference' assembly_filter = 'assembly' diff --git a/src/core/models/pages.py b/src/core/models/pages.py index 6577a977bd561dd7b120ce4875d26b06a0dcd8f7..1d5c480f93fddee344d22b24b9978c0cecbd8b1f 100644 --- a/src/core/models/pages.py +++ b/src/core/models/pages.py @@ -185,7 +185,7 @@ class StaticPageManager(models.Manager): return qs # content team can access non-public pages - if user.has_conference_staffpermission(conference, 'static_pages'): + if user.has_conference_staff_permission(conference, 'core.static_pages'): return qs # return only pages which are marked public @@ -231,7 +231,7 @@ class StaticPageManager(models.Manager): # If Page does not exist, User can create it if sp is None: - if group_check_failed and not user.has_conference_staffpermission(conference, 'static_pages'): + if group_check_failed and not user.has_conference_staff_permission(conference, 'core.static_pages'): return None, True new_page = StaticPage( @@ -243,7 +243,7 @@ class StaticPageManager(models.Manager): # If Page is protected, check if user has staff permission to ignore it if group_check_failed or sp.protection == StaticPage.Protection.PERM: - if not user.has_conference_staffpermission(conference, 'static_pages'): + if not user.has_conference_staff_permission(conference, 'core.static_pages'): return None, True return sp, True diff --git a/src/core/models/rooms.py b/src/core/models/rooms.py index c1c137845ba9350d4fb3557fe1e1fc106f15e3ad..2a1e393205b428293052c90cf5e0517b60b69b90 100644 --- a/src/core/models/rooms.py +++ b/src/core/models/rooms.py @@ -39,7 +39,7 @@ class RoomType(models.TextChoices): class RoomManager(ConferenceManagerMixin['Room']): - staff_permissions = ['assembly_team'] + staff_permissions = ['core.assembly_team'] conference_filter = 'assembly__conference' assembly_filter = 'assembly' @@ -446,7 +446,7 @@ class RoomShare(models.Model): class RoomLinkManager(ConferenceManagerMixin['RoomLink']): - staff_permissions = ['assembly_team'] + staff_permissions = ['core.assembly_team'] conference_filter = 'room__assembly__conference' assembly_filter = 'room__assembly' diff --git a/src/core/models/users.py b/src/core/models/users.py index b6b580e58d8d2ce51654d9fa5d171a955dbd4d90..e46cfcdffa6c61af0f14550fc8ea8b8f7a71d8d0 100644 --- a/src/core/models/users.py +++ b/src/core/models/users.py @@ -450,11 +450,17 @@ class PlatformUser(AbstractUser): return super().save(*args, update_fields=update_fields, **kwargs) - def has_conference_staffpermission(self, conference, *perms, need_all=False): - """ - Returns True if this user is a staff member of the given conference and has, - depending upon need_all, either at least one of the supplied permissions or all of them. - Global superuser and staff always return True. + def has_conference_staff_permission(self, conference: 'Conference', *perms: str, require_all: bool = False): + """Returns True if this user is a staff member of the given conference and has, + depending upon require_all, either at least one of the supplied permissions or all of them. + + Args: + conference (Conference): The Conference instance to check against. + *perms (str): The permissions to check for. + require_all (bool, optional): Defines, if all permission are required or just one. Defaults to False. + + Returns: + bool: True if the user has all permissions. """ if conference is None: @@ -463,7 +469,7 @@ class PlatformUser(AbstractUser): try: cm: ConferenceMember = conference.users.get(user=self) - return cm.has_staff_permission(*perms, need_all=need_all) + return cm.has_perms(*perms, require_all=require_all, require_staff=True) except ObjectDoesNotExist: return False diff --git a/src/core/models/workadventure.py b/src/core/models/workadventure.py index f96769d71dd2309e7cf1ae0b41b55ced70fe6f09..75f93037f8054af4e3f383ac20e94692de755dca 100644 --- a/src/core/models/workadventure.py +++ b/src/core/models/workadventure.py @@ -104,7 +104,7 @@ class WorkadventureSession(models.Model): # if this fails the user is no conference member and shall not be able to use the WA anyway conference_member = ConferenceMember.objects.get(conference_id=self.conference_id, user_id=self.user_id) - is_admin = conference_member.is_staff and conference_member.has_perm('core.workadventure_admin') + is_admin = conference_member.is_staff and conference_member.has_perms('core.workadventure_admin') is_angel = conference_member.active_angel user_tags = set(data.get('tags', [])) diff --git a/src/plainui/tests/test_views.py b/src/plainui/tests/test_views.py index 151c19846ac762190881e6700e7d31fc0f6ff89e..0fe5b37e36cebf5a549be6e1745160de762594d9 100644 --- a/src/plainui/tests/test_views.py +++ b/src/plainui/tests/test_views.py @@ -9,7 +9,7 @@ from freezegun import freeze_time from django import forms from django.contrib import messages -from django.contrib.auth.models import Permission +from django.contrib.auth.models import Group from django.contrib.contenttypes.models import ContentType from django.core import mail from django.core.exceptions import ImproperlyConfigured @@ -90,6 +90,10 @@ TEST_CONF_ID_3 = uuid.uuid4() @override_settings(SELECTED_CONFERENCE_ID=TEST_CONF_ID) class ViewsTestBase(TestCase): + fixtures = [ + 'bootstrap_auth_groups', + ] + def setUp(self): self.conf = Conference( id=TEST_CONF_ID, @@ -723,11 +727,11 @@ class ViewsTest(ViewsTestBase): self.conference_member.save() # namespace required, user doesn't have the required namespace but has static pages permission - perm, _unused = Permission.objects.get_or_create(content_type=ContentType.objects.get_for_model(ConferenceMember), codename='static_pages') + static_pages_group = Group.objects.get(name='Wiki Team') try: - self.user.user_permissions.add(perm) self.conference_member.is_staff = True self.conference_member.save() + self.conference_member.permission_groups.add(static_pages_group) resp = self.client.get(reverse('plainui:static_page_edit', kwargs={'page_slug': sp.slug})) self.assertEqual(resp.context_data['page'], sp) self.assertEqual(resp.context_data['page_slug'], sp.slug) @@ -737,7 +741,7 @@ class ViewsTest(ViewsTestBase): self.assertEqual(resp.context_data['form']['title'].value(), r2.title) self.assertEqual(resp.context_data['form']['body'].value(), r2.body) finally: - self.user.user_permissions.remove(perm) + self.conference_member.permission_groups.remove(static_pages_group) self.conference_member.is_staff = False self.conference_member.save() @@ -909,11 +913,10 @@ class ViewsTest(ViewsTestBase): self.conference_member.static_page_groups = [] self.conference_member.save() - # namespace required, user doesn't have that namespace but has static pages staff permission # namespace required, user doesn't have the required namespace but has static pages permission - perm, _unused = Permission.objects.get_or_create(content_type=ContentType.objects.get_for_model(ConferenceMember), codename='static_pages') + static_pages_group = Group.objects.get(name='Wiki Team') try: - self.user.user_permissions.add(perm) + self.conference_member.permission_groups.add(static_pages_group) self.conference_member.is_staff = True self.conference_member.save() lock = Lock( @@ -936,7 +939,7 @@ class ViewsTest(ViewsTestBase): self.assertEqual(sp.body, 'New Body5') self.assertEqual(sp_de.revisions.count(), 1) finally: - self.user.user_permissions.remove(perm) + self.conference_member.permission_groups.remove(static_pages_group) self.conference_member.is_staff = False self.conference_member.save() diff --git a/src/plainui/views/static_pages.py b/src/plainui/views/static_pages.py index e8052657ce8453896fab805dc9eb0aeacae056c1..97bb25a859bab2abe26f95ea9231da92cf2f5529 100644 --- a/src/plainui/views/static_pages.py +++ b/src/plainui/views/static_pages.py @@ -85,7 +85,7 @@ class StaticPageView(ConferenceRequiredMixin, TemplateView): return context if static_page.privacy == StaticPage.Privacy.PERM: - if not self.request.user.is_authenticated or not self.request.user.has_conference_staffpermission(self.conf, 'static_pages'): + if not self.request.user.is_authenticated or not self.request.user.has_conference_staff_permission(self.conf, 'core.static_pages'): context['page_no_permission'] = True context['page_can_edit'] = False context['page'] = None @@ -147,7 +147,7 @@ class StaticPageEditView(ConferenceRequiredMixin, TemplateView): # TODO: after redeem, redirect to edit view for page_slug return redirect(reverse('plainui:redeem_token')) - if static_page.privacy == StaticPage.Privacy.PERM and not self.request.user.has_conference_staffpermission(self.conf, 'static_pages'): + if static_page.privacy == StaticPage.Privacy.PERM and not self.request.user.has_conference_staff_permission(self.conf, 'core.static_pages'): messages.error(request, gettext('You do not have the required permissions to edit this page.')) return redirect(reverse('plainui:static_page', kwargs={'page_slug': page_slug})) @@ -239,7 +239,7 @@ class StaticPageEditView(ConferenceRequiredMixin, TemplateView): return redirect(reverse('plainui:static_page', kwargs={'page_slug': page_slug})) if static_page.privacy == StaticPage.Privacy.PERM: - if not self.request.user.has_conference_staffpermission(self.conf, 'static_pages'): + if not self.request.user.has_conference_staff_permission(self.conf, 'core.static_pages'): return redirect(reverse('plainui:static_page', kwargs={'page_slug': page_slug})) form = StaticPageBodyForm(request.POST) @@ -345,7 +345,7 @@ class StaticPageHistoryView(ConferenceRequiredMixin, TemplateView): return self.handle_no_permission() if self.static_page.privacy == StaticPage.Privacy.PERM: - if not self.request.user.is_authenticated or not self.request.user.has_conference_staffpermission(self.conf, 'static_pages'): + if not self.request.user.is_authenticated or not self.request.user.has_conference_staff_permission(self.conf, 'core.static_pages'): return redirect(reverse('plainui:static_page', kwargs={'page_slug': page_slug})) return super().get(request, page_slug=page_slug, **kwargs) @@ -373,7 +373,7 @@ class StaticPageDiffView(ConferenceRequiredMixin, TemplateView): return self.handle_no_permission() if self.static_page.privacy == StaticPage.Privacy.PERM: - if not self.request.user.is_authenticated or not self.request.user.has_conference_staffpermission(self.conf, 'static_pages'): + if not self.request.user.is_authenticated or not self.request.user.has_conference_staff_permission(self.conf, 'core.static_pages'): return redirect(reverse('plainui:static_page', kwargs={'page_slug': page_slug})) return super().get(request, page_slug=page_slug, **kwargs) @@ -420,7 +420,7 @@ class StaticPageGlobalHistoryView(ConferenceRequiredMixin, TemplateView): allowed_privacy = [StaticPage.Privacy.NONE] if self.has_ticket(self.request): allowed_privacy.append(StaticPage.Privacy.CONFERENCE) - if self.request.user.is_authenticated and self.request.user.has_conference_staffpermission(self.conf, 'static_pages'): + if self.request.user.is_authenticated and self.request.user.has_conference_staff_permission(self.conf, 'core.static_pages'): allowed_privacy.append(StaticPage.Privacy.PERM) static_pages = static_pages.filter(privacy__in=allowed_privacy)