diff options
author | Étienne Loks <etienne.loks@iggdrasil.net> | 2024-11-07 14:59:01 +0100 |
---|---|---|
committer | Étienne Loks <etienne.loks@iggdrasil.net> | 2025-02-19 14:43:49 +0100 |
commit | 89ff92664ff06a974e37c15ab663394271ac4a10 (patch) | |
tree | 48289b45207b4bd8ca73e7aaea116f15783df434 | |
parent | dbd8b853ce6bf5bc636f08448da8ff897963193e (diff) | |
download | Ishtar-89ff92664ff06a974e37c15ab663394271ac4a10.tar.bz2 Ishtar-89ff92664ff06a974e37c15ab663394271ac4a10.zip |
✨ update permission script - admin: delete "owns" groups when non relevent
-rw-r--r-- | ishtar_common/admin.py | 8 | ||||
-rw-r--r-- | ishtar_common/management/commands/ishtar_update_permissions.py | 66 | ||||
-rw-r--r-- | ishtar_common/migrations/0254_permissionrequests.py | 32 | ||||
-rw-r--r-- | ishtar_common/migrations/0255_migrate_delete_perm_clean_groups.py (renamed from ishtar_common/migrations/0255_migrate_delete_permissions.py) | 43 | ||||
-rw-r--r-- | ishtar_common/models.py | 113 | ||||
-rw-r--r-- | ishtar_common/models_common.py | 17 |
6 files changed, 270 insertions, 9 deletions
diff --git a/ishtar_common/admin.py b/ishtar_common/admin.py index 369821b45..465a9f152 100644 --- a/ishtar_common/admin.py +++ b/ishtar_common/admin.py @@ -1648,6 +1648,11 @@ class ProfileTypeAdmin(GeneralTypeAdmin): filter_vertical = ("groups",) autocomplete_fields = ("permission_requests",) + def save_related(self, request, form, formsets, change): + super().save_related(request, form, formsets, change) + # clean "owns" VS "generics" groups + form.instance.clean_groups() + def check_permission(self, request, object_id): # check that all "own" permission has a request associated try: @@ -1749,6 +1754,9 @@ admin_site.register(models.ProfileTypeSummary, ProfileTypeSummaryAdmin) class IshtarUserAdmin(admin.ModelAdmin): model = models.IshtarUser search_fields = ("user_ptr__username", "person__raw_name") + exclude = ("search_vector",) + readonly_fields = ("user_ptr", "latest_news_version",) + autocomplete_fields = ["person"] admin_site.register(models.IshtarUser, IshtarUserAdmin) diff --git a/ishtar_common/management/commands/ishtar_update_permissions.py b/ishtar_common/management/commands/ishtar_update_permissions.py new file mode 100644 index 000000000..d8d8944bf --- /dev/null +++ b/ishtar_common/management/commands/ishtar_update_permissions.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from argparse import RawTextHelpFormatter +import datetime +import sys + +from django.conf import settings +from django.core.management.base import BaseCommand + +from ishtar_common import models +from ishtar_common.utils import BColors + + +def update_permissions(quiet=False): + q = models.IshtarUser.objects.all() + nb = q.count() + updated = 0 + for idx, user in enumerate(q.all()): + if not quiet: + sys.stdout.write(f"\r[{percent(idx, nb)}] {idx + 1}/{nb}") + if user.need_permission_refresh(): + user.generate_permission() + updated += 1 + if not quiet: + msg = BColors.format("OKGREEN", f"\r{updated} user permissions updated") + sys.stdout.write(msg) + sys.stdout.write("\n") + + +def percent(current, total): + return f"{(current + 1) / total * 100:.1f}".rjust(4, "0") + "%" + + +def get_time(): + return datetime.datetime.now().isoformat().split(".")[0] + + +class Command(BaseCommand): + help = "Update permissions" + + def parser_error(self, message=""): + sys.stderr.write(f"{message}\n") + self.parser.print_help() + sys.exit(2) + + def create_parser(self, *args, **kwargs): + parser = super(Command, self).create_parser(*args, **kwargs) + parser.formatter_class = RawTextHelpFormatter + self.parser = parser + parser.error = self.parser_error + return parser + + def add_arguments(self, parser): + parser.add_argument( + "--quiet", dest="quiet", action="store_true", help="Quiet output" + ) + + def handle(self, *args, **options): + settings.USE_BACKGROUND_TASK = False + quiet = options["quiet"] + if not quiet: + msg = BColors.format("HEADER", f"[{get_time()}] Updating permissions\n") + sys.stdout.write(msg) + update_permissions(quiet) + sys.exit(1) diff --git a/ishtar_common/migrations/0254_permissionrequests.py b/ishtar_common/migrations/0254_permissionrequests.py index 68c4891b0..6d3435ddb 100644 --- a/ishtar_common/migrations/0254_permissionrequests.py +++ b/ishtar_common/migrations/0254_permissionrequests.py @@ -169,5 +169,35 @@ class Migration(migrations.Migration): model_name='userprofile', name='profile_type', field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='user_profiles', to='ishtar_common.ProfileType', verbose_name='Profile type') - ) + ), + migrations.AddField( + model_name='ishtaruser', + name='need_permission_update', + field=models.BooleanField(default=True, verbose_name='Need permission update'), + ), + migrations.AlterField( + model_name='biographicalnote', + name='ishtar_users', + field=models.ManyToManyField(blank=True, related_name='biographicalnote_associated', to='ishtar_common.IshtarUser'), + ), + migrations.AlterField( + model_name='document', + name='ishtar_users', + field=models.ManyToManyField(blank=True, related_name='document_associated', to='ishtar_common.IshtarUser'), + ), + migrations.AlterField( + model_name='organization', + name='ishtar_users', + field=models.ManyToManyField(blank=True, related_name='organization_associated', to='ishtar_common.IshtarUser'), + ), + migrations.AlterField( + model_name='person', + name='ishtar_users', + field=models.ManyToManyField(blank=True, related_name='person_associated', to='ishtar_common.IshtarUser'), + ), + migrations.AlterField( + model_name='profiletype', + name='groups', + field=models.ManyToManyField(blank=True, related_name='profile_types', to='auth.Group', verbose_name='Groups'), + ), ] diff --git a/ishtar_common/migrations/0255_migrate_delete_permissions.py b/ishtar_common/migrations/0255_migrate_delete_perm_clean_groups.py index 61b63c0df..d9aa4cd32 100644 --- a/ishtar_common/migrations/0255_migrate_delete_permissions.py +++ b/ishtar_common/migrations/0255_migrate_delete_perm_clean_groups.py @@ -3,7 +3,46 @@ from django.db import migrations +def clean_groups(profile_type): + # raw copy of the admin code + owns, full = {}, [] + # get all permissions + for group in profile_type.groups.all(): + permissions = [] + own, gen = False, False + q = group.permissions + if not q.count(): + continue + for permission in q.all(): + if "_own_" in permission.codename: + own = True + else: + gen = True + parts = permission.codename.split("_") + permissions.append(f"{parts[0]}_{parts[-1]}") + if own and gen: + # group has "own" and "generic" permissions: do nothing + continue + permissions = tuple(sorted(permissions)) + if own: + owns[permissions] = group + else: + full.append(permissions) + # clean + for permissions in owns.keys(): + if len(permissions) == 1: + for full_permissions in full: + for full_permission in full_permissions: + if full_permission == permissions[0]: + profile_type.groups.remove(owns[permissions]) + break + else: + if permissions in full: + profile_type.groups.remove(owns[permissions]) + + def migrate_permission(apps, __): + # clean delete permissions Permission = apps.get_model("auth", "permission") Group = apps.get_model("auth", "group") ProfileType = apps.get_model("ishtar_common", "profiletype") @@ -39,6 +78,10 @@ def migrate_permission(apps, __): for profile_type in ProfileType.objects.filter(groups__pk=modif_group.pk).all(): profile_type.groups.add(delete_group) print(f"\t- profile type {profile_type.label} updated") + # clean groups + ProfileType = apps.get_model("ishtar_common", "ProfileType") + for pt in ProfileType.objects.all(): + clean_groups(pt) class Migration(migrations.Migration): diff --git a/ishtar_common/models.py b/ishtar_common/models.py index 1569c97c9..045bab1cc 100644 --- a/ishtar_common/models.py +++ b/ishtar_common/models.py @@ -54,7 +54,7 @@ from xml.etree import ElementTree as ET # nosec from django.apps import apps from django.conf import settings -from django.contrib.auth.models import User, Group +from django.contrib.auth.models import User, Group, Permission from django.contrib.contenttypes.models import ContentType from django.contrib.gis.db import models from django.contrib.gis.db.models.aggregates import Union @@ -73,7 +73,7 @@ from django.core.files.base import ContentFile from django.core.files.uploadedfile import SimpleUploadedFile from django.db import connection, transaction from django.db.models import Q, Max, Count -from django.db.models.signals import post_save, post_delete, m2m_changed +from django.db.models.signals import post_save, post_delete, pre_delete, m2m_changed from django.db.utils import DatabaseError from django.template import Context, Template from django.template.defaultfilters import slugify @@ -3453,11 +3453,65 @@ class ProfileType(GeneralType): ordering = ("label",) ADMIN_SECTION = _("Account") + def clean_groups(self): + """ + Remove "own" groups if generic group is associated + """ + owns, full = {}, [] + # get all permissions + for group in self.groups.all(): + permissions = [] + own, gen = False, False + q = group.permissions + if not q.count(): + continue + for permission in q.all(): + if "_own_" in permission.codename: + own = True + else: + gen = True + parts = permission.codename.split("_") + permissions.append(f"{parts[0]}_{parts[-1]}") + if own and gen: + # group has "own" and "generic" permissions: do nothing + continue + permissions = tuple(sorted(permissions)) + if own: + owns[permissions] = group + else: + full.append(permissions) + # clean + for permissions in owns.keys(): + if len(permissions) == 1: + for full_permissions in full: + for full_permission in full_permissions: + if full_permission == permissions[0]: + self.groups.remove(owns[permissions]) + break + else: + if permissions in full: + self.groups.remove(owns[permissions]) + post_save.connect(post_save_cache, sender=ProfileType) post_delete.connect(post_save_cache, sender=ProfileType) +def permission_requests_changed(sender, **kwargs): + instance = kwargs.get("instance", None) + if not instance: + return + IshtarUser.objects.filter( + person__profiles__profile_type_id=instance.id + ).update(need_permission_update=True) + + +m2m_changed.connect(permission_requests_changed, + sender=ProfileType.permission_requests.through) +m2m_changed.connect(permission_requests_changed, + sender=ProfileType.groups.through) + + class ProfileTypeSummary(ProfileType): class Meta: proxy = True @@ -3567,12 +3621,14 @@ class UserProfile(models.Model): item_ids += list( Find.objects.filter(**{k: ishtar_user}).values_list("pk", flat=True) ) - print("ishtar_common/models.py - 3561", item_ids, ishtar_user, content_type, permission_type) + # DEBUG + # print("ishtar_common/models.py - 3578", item_ids, ishtar_user, content_type, permission_type) if permission_request.include_upstream_items: item_ids += model_class.get_ids_from_upper_permissions( ishtar_user.user_ptr.pk, permissions ) - print("ishtar_common/models.py - 3566", item_ids, ishtar_user, content_type, permission_type) + # DEBUG + # print("ishtar_common/models.py - 3584", item_ids, ishtar_user, content_type, permission_type) if permission_request.request or permission_request.limit_to_attached_areas: _get_item = get_item( content_type.model_class(), @@ -3606,7 +3662,8 @@ class UserProfile(models.Model): else: result = result_limit item_ids += result - print("ishtar_common/models.py - 3600", item_ids, ishtar_user, content_type, permission_type) + # DEBUG + # print("ishtar_common/models.py - 3619", item_ids, ishtar_user, content_type, permission_type) return item_ids def generate_permission(self, content_type, permission_type, @@ -3643,8 +3700,9 @@ class UserProfile(models.Model): item_ids = [] if not q_req.count(): # TODO v5: delete old behaviour - print(f"WARNING: no permission request for content {content_type.name} and profile {self}") - print("Using old behaviour") + # DEBUG + # print(f"WARNING: no permission request for content {content_type.name} and profile {self}") + # print("Using old behaviour") model_class = content_type.model_class() query = model_class.get_owns(user=ishtar_user, query=True, no_auth_check=True) if query: @@ -3711,6 +3769,9 @@ def post_save_userprofile(sender, **kwargs): if not kwargs.get("instance"): return instance = kwargs.get("instance") + IshtarUser.objects.filter( + person__profiles__pk=instance.id + ).update(need_permission_update=True) try: instance.person.ishtaruser.show_field_number(update=True) except IshtarUser.DoesNotExist: @@ -3720,6 +3781,18 @@ def post_save_userprofile(sender, **kwargs): post_save.connect(post_save_userprofile, sender=UserProfile) +def pre_delete_user_profile(sender, **kwargs): + instance = kwargs.get("instance", None) + if not instance: + return + IshtarUser.objects.filter( + person__profiles__pk=instance.id + ).update(need_permission_update=True) + + +pre_delete.connect(pre_delete_user_profile, sender=UserProfile) + + TASK_STATE = ( ("S", _("Scheduled")), ("P", _("In progress")), @@ -3837,6 +3910,9 @@ class IshtarUser(FullSearch): blank=True, max_length=20) display_news = models.BooleanField(_("Display news"), default=True) display_forum_entries = models.BooleanField(_("Display forum entries"), default=True) + # permissions update + need_permission_update = models.BooleanField(_("Need permission update"), + default=True) class Meta: verbose_name = _("Ishtar user") @@ -3929,6 +4005,26 @@ class IshtarUser(FullSearch): return self.user_ptr.has_perm(permission, obj) return self.user_ptr.has_perm(permission) + def need_permission_refresh(self): + if self.need_permission_update: + return True + q = UserProfile.objects.filter( + person_id=self.person_id, + expiration_date__lt=datetime.date.today() + ) + if q.count(): + self.need_permission_update = True + self.save() + return True + q = Permission.objects.filter( + group__profile_types__user_profiles__person__ishtaruser__pk=self.pk, + codename__contains="_own_", + ) + if q.count(): + self.need_permission_update = True + self.save() + return bool(q.count()) + def generate_permission(self): # models to treat first in this order to manage cascade permissions model_names = [ @@ -3971,6 +4067,9 @@ class IshtarUser(FullSearch): for permission_type in ("view", "change", "delete"): profile.generate_permission(ct, permission_type) + self.need_permission_update = False + self.save() + def has_permission_dict(self): """ Get permission dict with permission codename as key and True or False as result. diff --git a/ishtar_common/models_common.py b/ishtar_common/models_common.py index ed2d46305..dc48fa9e5 100644 --- a/ishtar_common/models_common.py +++ b/ishtar_common/models_common.py @@ -2466,8 +2466,12 @@ class GeoVectorData(Imported, OwnPerms): sub_q = cls._construct_query_own( "", model._get_query_owns_dicts(ishtaruser) ) + if not sub_q: + continue q2 = Q( - source_id__in=list(sub_q.values_list("id", flat=True)), + source_id__in=list( + model.objects.filter(sub_q).values_list("id", flat=True) + ), source_content_type__app_label=app_label, source_content_type__model=model_name.lower(), ) @@ -3127,6 +3131,17 @@ class PermissionRequest(models.Model): return f"{self.model} - {self.name}" +def post_save_permission_request(sender, **kwargs): + permission_request = kwargs["instance"] + IshtarUser = apps.get_model("ishtar_common", "IshtarUser") + IshtarUser.objects.filter( + person__profiles__profile_type__permission_requests__pk=permission_request.pk + ).update(need_permission_update=True) + + +post_save.connect(post_save_permission_request, sender=PermissionRequest) + + class SerializeItem: SERIALIZE_EXCLUDE = ["search_vector"] SERIALIZE_PROPERTIES = [ |