diff options
author | Étienne Loks <etienne.loks@iggdrasil.net> | 2024-10-23 18:51:15 +0200 |
---|---|---|
committer | Étienne Loks <etienne.loks@iggdrasil.net> | 2025-02-19 14:43:48 +0100 |
commit | 6f59b9e36a0971b3deb44562062a878eb26beedf (patch) | |
tree | e22db164f77fc0ba6e30a539350bb5a37f36f5a6 | |
parent | be063a7032971db7c00a160595e69e1e67dd2c9f (diff) | |
download | Ishtar-6f59b9e36a0971b3deb44562062a878eb26beedf.tar.bz2 Ishtar-6f59b9e36a0971b3deb44562062a878eb26beedf.zip |
✨ permissions refactoring: generate permissions, adapt permissions checks
-rw-r--r-- | archaeological_context_records/models.py | 7 | ||||
-rw-r--r-- | archaeological_context_records/tests.py | 73 | ||||
-rw-r--r-- | archaeological_context_records/wizards.py | 2 | ||||
-rw-r--r-- | archaeological_files/models.py | 8 | ||||
-rw-r--r-- | archaeological_files/wizards.py | 2 | ||||
-rw-r--r-- | archaeological_finds/models_finds.py | 7 | ||||
-rw-r--r-- | archaeological_finds/models_treatments.py | 15 | ||||
-rw-r--r-- | archaeological_finds/tests.py | 40 | ||||
-rw-r--r-- | archaeological_finds/wizards.py | 6 | ||||
-rw-r--r-- | archaeological_operations/models.py | 14 | ||||
-rw-r--r-- | archaeological_operations/tests.py | 70 | ||||
-rw-r--r-- | archaeological_operations/views.py | 5 | ||||
-rw-r--r-- | archaeological_operations/wizards.py | 10 | ||||
-rw-r--r-- | archaeological_warehouse/tests.py | 24 | ||||
-rw-r--r-- | ishtar_common/admin.py | 3 | ||||
-rw-r--r-- | ishtar_common/models.py | 154 | ||||
-rw-r--r-- | ishtar_common/models_common.py | 7 | ||||
-rw-r--r-- | ishtar_common/utils.py | 14 | ||||
-rw-r--r-- | ishtar_common/views_item.py | 1 | ||||
-rw-r--r-- | ishtar_common/wizards.py | 36 |
20 files changed, 389 insertions, 109 deletions
diff --git a/archaeological_context_records/models.py b/archaeological_context_records/models.py index 2611b7dfd..a88a7d161 100644 --- a/archaeological_context_records/models.py +++ b/archaeological_context_records/models.py @@ -1234,7 +1234,8 @@ class ContextRecord( @classmethod def get_owns( - cls, user, menu_filtr=None, limit=None, values=None, get_short_menu_class=None + cls, user, menu_filtr=None, limit=None, values=None, get_short_menu_class=None, + no_auth_check=False, query=False ): replace_query = None if menu_filtr and "operation" in menu_filtr: @@ -1245,7 +1246,11 @@ class ContextRecord( limit=limit, values=values, get_short_menu_class=get_short_menu_class, + no_auth_check=no_auth_check, + query=query ) + if query: + return owns return cls._return_get_owns(owns, values, get_short_menu_class) def full_label(self): diff --git a/archaeological_context_records/tests.py b/archaeological_context_records/tests.py index efeab4670..8a16facf5 100644 --- a/archaeological_context_records/tests.py +++ b/archaeological_context_records/tests.py @@ -23,7 +23,7 @@ import locale from django.apps import apps from django.conf import settings -from django.contrib.auth.models import Permission +from django.contrib.auth.models import Permission, Group from django.core.exceptions import ValidationError from django.core.files.uploadedfile import SimpleUploadedFile from django.template.defaultfilters import slugify @@ -32,14 +32,15 @@ from django.urls import reverse from django.utils.translation import pgettext_lazy from ishtar_common.models import ( - IshtarSiteProfile, + Area, + get_current_profile, ImporterModel, ImporterType, - UserProfile, + IshtarSiteProfile, + IshtarUser, ProfileType, Town, - Area, - get_current_profile, + UserProfile, ) from ishtar_common import forms_common @@ -945,21 +946,35 @@ class ContextRecordPermissionTest(ContextRecordInit, TestCase): fixtures = CONTEXT_RECORD_TOWNS_FIXTURES def setUp(self): - IshtarSiteProfile.objects.create() - self.username, self.password, self.user = create_superuser() - self.alt_username, self.alt_password, self.alt_user = create_user() - self.alt_user.user_permissions.add( + profile_type = ProfileType.objects.create( + label="xxCollaborateur", + txt_idx="xxcollaborator", + ) + gp = Group.objects.create(name="xxUE rattachées : voir et modification") + gp.permissions.add( Permission.objects.get(codename="view_own_contextrecord") ) - self.alt_user.user_permissions.add( + gp.permissions.add( Permission.objects.get(codename="change_own_contextrecord") ) + profile_type.groups.add(gp) + + IshtarSiteProfile.objects.create() + self.username, self.password, self.user = create_superuser() + self.alt_username, self.alt_password, self.alt_user = create_user() + + UserProfile.objects.create( + profile_type=profile_type, + person=self.alt_user.ishtaruser.person, + current=True, + ) + # nosec: hard coded password for test purposes self.alt_username2, self.alt_password2, self.alt_user2 = create_user( # nosec username="luke", password="iamyourfather" ) profile = UserProfile.objects.create( - profile_type=ProfileType.objects.get(txt_idx="collaborator"), + profile_type=profile_type, person=self.alt_user2.ishtaruser.person, current=True, ) @@ -984,6 +999,9 @@ class ContextRecordPermissionTest(ContextRecordInit, TestCase): self.cr_1 = self.context_records[0] self.cr_2 = self.context_records[1] + self.alt_user.ishtaruser.generate_permission() + self.alt_user2.ishtaruser.generate_permission() + def test_own_search(self): # no result when no authentification c = Client() @@ -1390,29 +1408,42 @@ class ContextRecordRelationTest(ContextRecordInit, TestCase): fixtures = CONTEXT_RECORD_TOWNS_FIXTURES def setUp(self): - # nosec: hard coded password for test purposes - self.username, self.password, user = create_user( # nosec - username="Gandalf", password="ushallpass" + profile_type = ProfileType.objects.create( + label="xxCollaborateur", + txt_idx="xxcollaborator", ) - user.user_permissions.add( + gp = Group.objects.create(name="xxUE rattachées : voir et modification") + gp.permissions.add( Permission.objects.get(codename="view_own_contextrecord") ) - user.user_permissions.add( + gp.permissions.add( Permission.objects.get(codename="change_own_contextrecord") ) + profile_type.groups.add(gp) + + # nosec: hard coded password for test purposes + self.username, self.password, user = create_user( # nosec + username="Gandalf", password="ushallpass" + ) + UserProfile.objects.create( + profile_type=profile_type, + person=user.ishtaruser.person, + current=True, + ) self.create_context_record({"label": "CR 1"}, user=user) self.create_context_record({"label": "CR 2"}, user=user) self.create_context_record({"label": "CR 3"}, user=user) + IshtarUser.objects.get(pk=user.pk).generate_permission() self.username2, self.password2, user2 = create_user( # nosec username="Saroumane", password="ushallnotpass" ) - user2.user_permissions.add( - Permission.objects.get(codename="view_own_contextrecord") - ) - user2.user_permissions.add( - Permission.objects.get(codename="change_own_contextrecord") + UserProfile.objects.create( + profile_type=profile_type, + person=user2.ishtaruser.person, + current=True, ) + IshtarUser.objects.get(pk=user2.pk).generate_permission() self.cr1, self.cr2, self.cr3 = self.context_records self.sym_rel_type, __ = models.RelationType.objects.get_or_create( symmetrical=True, txt_idx="sym", logical_relation="equal" diff --git a/archaeological_context_records/wizards.py b/archaeological_context_records/wizards.py index 35b4e02b4..4524681fc 100644 --- a/archaeological_context_records/wizards.py +++ b/archaeological_context_records/wizards.py @@ -31,7 +31,7 @@ class RecordSearch(SearchWizard): class RecordWizard(Wizard): model = models.ContextRecord - edit = False + modification = False wizard_done_window = reverse_lazy("show-contextrecord") relations_step_key = "relations" redirect_url = "record_modification" diff --git a/archaeological_files/models.py b/archaeological_files/models.py index 1f164c30d..778b1c251 100644 --- a/archaeological_files/models.py +++ b/archaeological_files/models.py @@ -1060,11 +1060,15 @@ class File( @classmethod def get_owns( - cls, user, menu_filtr=None, limit=None, values=None, get_short_menu_class=False + cls, user, menu_filtr=None, limit=None, values=None, get_short_menu_class=False, + no_auth_check=False, query=False ): owns = super(File, cls).get_owns( - user, limit=limit, values=values, get_short_menu_class=get_short_menu_class + user, limit=limit, values=values, get_short_menu_class=get_short_menu_class, + no_auth_check=no_auth_check, query=query ) + if query: + return owns return cls._return_get_owns(owns, values, get_short_menu_class) def get_dynamic_values(self, prefix, values, filtr=None): diff --git a/archaeological_files/wizards.py b/archaeological_files/wizards.py index 8178e1c11..dd3f1e828 100644 --- a/archaeological_files/wizards.py +++ b/archaeological_files/wizards.py @@ -242,7 +242,7 @@ class FileAdministrativeActWizard(OperationAdministrativeActWizard): class FileEditAdministrativeActWizard(FileAdministrativeActWizard): model = AdministrativeAct - edit = True + modification = True def get_associated_item(self, dct): return self.get_current_object().associated_file diff --git a/archaeological_finds/models_finds.py b/archaeological_finds/models_finds.py index 1863ba450..9ba25cc83 100644 --- a/archaeological_finds/models_finds.py +++ b/archaeological_finds/models_finds.py @@ -3023,7 +3023,8 @@ class Find( @classmethod def get_owns( - cls, user, menu_filtr=None, limit=None, values=None, get_short_menu_class=None + cls, user, menu_filtr=None, limit=None, values=None, get_short_menu_class=None, + no_auth_check=False, query=False ): replace_query = None if menu_filtr and "contextrecord" in menu_filtr: @@ -3034,7 +3035,11 @@ class Find( limit=limit, values=values, get_short_menu_class=get_short_menu_class, + no_auth_check=no_auth_check, + query=query ) + if query: + return owns return cls._return_get_owns(owns, values, get_short_menu_class) def _generate_cached_label(self): diff --git a/archaeological_finds/models_treatments.py b/archaeological_finds/models_treatments.py index d23843226..5ba50728b 100644 --- a/archaeological_finds/models_treatments.py +++ b/archaeological_finds/models_treatments.py @@ -341,7 +341,8 @@ class Treatment( @classmethod def get_owns( - cls, user, menu_filtr=None, limit=None, values=None, get_short_menu_class=None + cls, user, menu_filtr=None, limit=None, values=None, get_short_menu_class=None, + no_auth_check=False, query=False ): replace_query = None if menu_filtr: @@ -356,7 +357,11 @@ class Treatment( limit=limit, values=values, get_short_menu_class=get_short_menu_class, + no_auth_check=no_auth_check, + query=query ) + if query: + return owns return cls._return_get_owns(owns, values, get_short_menu_class) def get_query_operations(self): @@ -1346,11 +1351,15 @@ class TreatmentFile( @classmethod def get_owns( - cls, user, menu_filtr=None, limit=None, values=None, get_short_menu_class=None + cls, user, menu_filtr=None, limit=None, values=None, get_short_menu_class=None, + no_auth_check=False, query=False ): owns = super(TreatmentFile, cls).get_owns( - user, limit=limit, values=values, get_short_menu_class=get_short_menu_class + user, limit=limit, values=values, get_short_menu_class=get_short_menu_class, + no_auth_check=no_auth_check, query=query ) + if query: + return owns return cls._return_get_owns(owns, values, get_short_menu_class) def _generate_cached_label(self): diff --git a/archaeological_finds/tests.py b/archaeological_finds/tests.py index 16fb575c7..5df18cf64 100644 --- a/archaeological_finds/tests.py +++ b/archaeological_finds/tests.py @@ -27,7 +27,7 @@ from rest_framework.test import APITestCase from rest_framework.authtoken.models import Token from django.conf import settings -from django.contrib.auth.models import User, Permission, ContentType +from django.contrib.auth.models import User, Permission, ContentType, Group from django.core.files import File from django.core.files.uploadedfile import SimpleUploadedFile from django.db.utils import IntegrityError @@ -1923,23 +1923,42 @@ class FindPermissionTest(FindInit, TestCase): model = models.Find def setUp(self): - self.username, self.password, self.user = create_superuser() - self.alt_username, self.alt_password, self.alt_user = create_user() + profile_type = ProfileType.objects.create( + label="xxCollaborateur", + txt_idx="xxcollaborator", + ) + gp = Group.objects.create(name="xxMobilier rattachées : voir et modification") ct_find = ContentType.objects.get( app_label="archaeological_finds", model="find" ) - self.alt_user.user_permissions.add( - Permission.objects.get(codename="view_own_find", content_type=ct_find) + gp.permissions.add( + Permission.objects.get( + codename="view_own_find", + content_type=ct_find + ) ) - self.alt_user.user_permissions.add( - Permission.objects.get(codename="change_own_find", content_type=ct_find) + gp.permissions.add( + Permission.objects.get( + codename="change_own_find", + content_type=ct_find + ) ) + profile_type.groups.add(gp) + + self.username, self.password, self.user = create_superuser() + self.alt_username, self.alt_password, self.alt_user = create_user() + profile = UserProfile.objects.create( + profile_type=profile_type, + person=self.alt_user.ishtaruser.person, + current=True, + ) + # nosec: hard coded password for test purposes self.alt_username2, self.alt_password2, self.alt_user2 = create_user( # nosec username="luke", password="iamyourfather" ) profile = UserProfile.objects.create( - profile_type=ProfileType.objects.get(txt_idx="collaborator"), + profile_type=profile_type, person=self.alt_user2.ishtaruser.person, current=True, ) @@ -1973,6 +1992,9 @@ class FindPermissionTest(FindInit, TestCase): self.find_2 = self.finds[-1] self.operations[-1].towns.add(town) + self.alt_user.ishtaruser.generate_permission() + self.alt_user2.ishtaruser.generate_permission() + def test_own_search(self): # no result when no authentification c = Client() @@ -1985,7 +2007,7 @@ class FindPermissionTest(FindInit, TestCase): response = c.get(reverse("get-find")) # only one "own" context record available content = response.content.decode() - self.assertTrue(json.loads(content)) + self.assertTrue(content) self.assertEqual(json.loads(content)["recordsTotal"], 1) # area filter diff --git a/archaeological_finds/wizards.py b/archaeological_finds/wizards.py index d6647b81c..c70721eac 100644 --- a/archaeological_finds/wizards.py +++ b/archaeological_finds/wizards.py @@ -506,7 +506,7 @@ class TreatmentAdministrativeActWizard(OperationAdministrativeActWizard): class TreatmentEditAdministrativeActWizard(TreatmentAdministrativeActWizard): model = AdministrativeAct - edit = True + modification = True def get_associated_item(self, dct): return self.get_current_object().treatment @@ -583,7 +583,7 @@ class TreatmentFileAdministrativeActWizard(OperationAdministrativeActWizard): class TreatmentFileEditAdministrativeActWizard(TreatmentFileAdministrativeActWizard): model = AdministrativeAct - edit = True + modification = True def get_associated_item(self, dct): return self.get_current_object().treatment_file @@ -600,7 +600,7 @@ class FindBasketWizard(Wizard): class FindBasketEditWizard(FindBasketWizard): - edit = True + modification = True alt_is_own_method = "get_write_query_owns" def get_form_kwargs(self, step, **kwargs): diff --git a/archaeological_operations/models.py b/archaeological_operations/models.py index a074adc9a..2fafa56ed 100644 --- a/archaeological_operations/models.py +++ b/archaeological_operations/models.py @@ -902,7 +902,8 @@ class ArchaeologicalSite( @classmethod def get_owns( - cls, user, menu_filtr=None, limit=None, values=None, get_short_menu_class=None + cls, user, menu_filtr=None, limit=None, values=None, get_short_menu_class=None, + no_auth_check=False, query=False ): replace_query = None if menu_filtr and "operation" in menu_filtr: @@ -914,7 +915,11 @@ class ArchaeologicalSite( limit=limit, values=values, get_short_menu_class=get_short_menu_class, + no_auth_check=no_auth_check, + query=query ) + if query: + return owns return cls._return_get_owns(owns, values, get_short_menu_class) def _generate_cached_label(self): @@ -1782,7 +1787,8 @@ class Operation( @classmethod def get_owns( - cls, user, menu_filtr=None, limit=None, values=None, get_short_menu_class=None + cls, user, menu_filtr=None, limit=None, values=None, get_short_menu_class=None, + no_auth_check=False, query=False ): replace_query = None if menu_filtr and "file" in menu_filtr: @@ -1794,7 +1800,11 @@ class Operation( limit=limit, values=values, get_short_menu_class=get_short_menu_class, + no_auth_check=no_auth_check, + query=query ) + if query: + return owns return cls._return_get_owns(owns, values, get_short_menu_class) def __str__(self): diff --git a/archaeological_operations/tests.py b/archaeological_operations/tests.py index b0b9c9dae..2c5946e87 100644 --- a/archaeological_operations/tests.py +++ b/archaeological_operations/tests.py @@ -3480,18 +3480,26 @@ class OperationPermissionTest(TestCase, OperationInitTest): def setUp(self): IshtarSiteProfile.objects.get_or_create(slug="default", active=True) self.username, self.password, self.user = create_superuser() + self.alt_username, self.alt_password, self.alt_user = create_user() - self.alt_user.user_permissions.add( - Permission.objects.get(codename="view_own_operation") + profile_type = ProfileType.objects.create( + label="xxCollaborateur", + txt_idx="xxcollaborator", ) - self.alt_user.user_permissions.add( - Permission.objects.get(codename="change_own_operation") + UserProfile.objects.create( + profile_type=profile_type, + person=self.alt_user.ishtaruser.person, + current=True, ) + gp = Group.objects.create(name="xxOpérations rattachées : voir et modification") + gp.permissions.add(Permission.objects.get(codename="view_own_operation")) + gp.permissions.add(Permission.objects.get(codename="change_own_operation")) + profile_type.groups.add(gp) + # nosec: hard coded password for test purposes self.alt_username2, self.alt_password2, self.alt_user2 = create_user( # nosec username="luke", password="iamyourfather" ) - profile_type = ProfileType.objects.get(txt_idx="collaborator") profile = UserProfile.objects.create( profile_type=profile_type, person=self.alt_user2.ishtaruser.person, @@ -3503,11 +3511,51 @@ class OperationPermissionTest(TestCase, OperationInitTest): profile.areas.add(area) self.orgas = self.create_orgas(self.user) - self.operations = self.create_operation(self.user, self.orgas[0]) - self.operations += self.create_operation(self.alt_user, self.orgas[0]) + self.create_operation(self.user, self.orgas[0]) + self.operations = self.create_operation(self.alt_user, self.orgas[0]) self.operations[1].towns.add(town) self.item = self.operations[0] + def test_permission_generation(self): + alt_user = IshtarUser.objects.get(pk=self.alt_user.pk) + self.assertFalse( + alt_user.user_ptr.has_perm( + "archaeological_operations.change_own_operation", + self.operations[1] + ) + ) + self.assertFalse( + alt_user.user_ptr.has_perm( + "archaeological_operations.view_own_operation", + self.operations[1] + ) + ) + alt_user.generate_permission() + self.assertTrue( + alt_user.user_ptr.has_perm( + "archaeological_operations.view_own_operation", + self.operations[1] + ) + ) + self.assertTrue( + alt_user.user_ptr.has_perm( + "archaeological_operations.change_own_operation", + self.operations[1] + ) + ) + # general permission is assigned + self.assertTrue( + alt_user.user_ptr.has_perm( + "archaeological_operations.change_own_operation", + ) + ) + self.assertFalse( + alt_user.user_ptr.has_perm( + "archaeological_operations.change_own_operation", + self.operations[0] + ) + ) + def test_own_search(self): # no result when no authentification c = Client() @@ -3515,6 +3563,8 @@ class OperationPermissionTest(TestCase, OperationInitTest): self.assertTrue(not json.loads(response.content.decode())) # possession + alt_user = IshtarUser.objects.get(pk=self.alt_user.pk) + alt_user.generate_permission() c = Client() c.login(username=self.alt_username, password=self.alt_password) response = c.get(reverse("get-operation"), {"year": "2010"}) @@ -3531,6 +3581,8 @@ class OperationPermissionTest(TestCase, OperationInitTest): self.assertEqual(json.loads(response.content.decode())["recordsTotal"], 1) # area filter + alt_user2 = IshtarUser.objects.get(pk=self.alt_user2.pk) + alt_user2.generate_permission() c = Client() c.login(username=self.alt_username2, password=self.alt_password2) response = c.get(reverse("get-operation"), {"year": "2010"}) @@ -3557,6 +3609,8 @@ class OperationPermissionTest(TestCase, OperationInitTest): self.assertRedirects(response, "/") # possession + alt_user = IshtarUser.objects.get(pk=self.alt_user.pk) + alt_user.generate_permission() c = Client() c.login(username=self.alt_username, password=self.alt_password) response = c.get(reverse("operation_modify", args=[operation_pk2]), follow=True) @@ -3574,6 +3628,8 @@ class OperationPermissionTest(TestCase, OperationInitTest): ) # area filter + alt_user2 = IshtarUser.objects.get(pk=self.alt_user2.pk) + alt_user2.generate_permission() c = Client() c.login(username=self.alt_username2, password=self.alt_password2) response = c.get(reverse("operation_modify", args=[operation_pk2]), follow=True) diff --git a/archaeological_operations/views.py b/archaeological_operations/views.py index 7b6a56597..270411bfd 100644 --- a/archaeological_operations/views.py +++ b/archaeological_operations/views.py @@ -540,7 +540,10 @@ def get_relation_modify(model, model_relation, formset_class, url_name, except model.DoesNotExist: raise Http404() if "_own_" in current_right: - if not item.is_own(request.user): + if not request.user.has_perm(current_right, item): + raise PermissionDenied() + elif current_right: + if not request.user.has_perm(current_right): raise PermissionDenied() relations = model_relation.objects.filter(left_record_id=pk).all() form_kwargs = {"left_record": item} diff --git a/archaeological_operations/wizards.py b/archaeological_operations/wizards.py index ac8aaf40d..b510aa4cc 100644 --- a/archaeological_operations/wizards.py +++ b/archaeological_operations/wizards.py @@ -21,7 +21,6 @@ import logging from django.conf import settings from django.core.exceptions import ObjectDoesNotExist -from django.http import Http404 from django.shortcuts import render from django.urls import reverse from ishtar_common.utils import ugettext_lazy as _ @@ -34,7 +33,6 @@ from ishtar_common.models import get_current_profile from ishtar_common.wizards import ( Wizard, ClosingWizard, - DeletionWizard, SearchWizard, MultipleDeletionWizard, ) @@ -240,7 +238,7 @@ class OperationDeletionWizard(MultipleDeletionWizard): class OperationAdministrativeActWizard(OperationWizard): - edit = False + modification = False wizard_done_window = reverse_lazy("show-administrativeact") current_obj_slug = "administrativeactop" ref_object_key = "operation" @@ -303,7 +301,7 @@ class OperationAdministrativeActWizard(OperationWizard): dct["history_modifier"] = self.request.user if "pk" in dct: dct.pop("pk") - if self.edit: + if self.modification: admact = self.get_current_object() for k in dct: if hasattr(admact, k): @@ -358,7 +356,7 @@ class OperationAdministrativeActWizard(OperationWizard): return res def get_form_initial(self, step, data=None): - if not self.edit: + if not self.modification: return {} initial = super().get_form_initial(step) return initial @@ -366,7 +364,7 @@ class OperationAdministrativeActWizard(OperationWizard): class OperationEditAdministrativeActWizard(OperationAdministrativeActWizard): model = models.AdministrativeAct - edit = True + modification = True def get_associated_item(self, dct): return self.get_current_object().operation diff --git a/archaeological_warehouse/tests.py b/archaeological_warehouse/tests.py index 96df83024..e92298c9f 100644 --- a/archaeological_warehouse/tests.py +++ b/archaeological_warehouse/tests.py @@ -20,7 +20,7 @@ import datetime import json -from django.contrib.auth.models import Permission, User +from django.contrib.auth.models import Permission, Group from django.db.utils import IntegrityError from django.test.client import Client from django.urls import reverse @@ -38,10 +38,11 @@ from ishtar_common.tests import ( create_superuser, ) -from ishtar_common.models import IshtarSiteProfile +from ishtar_common.models import IshtarSiteProfile, ProfileType, UserProfile from archaeological_operations.models import Operation from archaeological_context_records.models import ContextRecord -from archaeological_finds.models import Find, MaterialType, Treatment, TreatmentType, FindTreatment, TreatmentState +from archaeological_finds.models import \ + Find, MaterialType, Treatment, TreatmentType, FindTreatment, TreatmentState from archaeological_warehouse import models, views, forms, serializers @@ -808,7 +809,22 @@ class ContainerTest(FindInit, TestCase): container_2.index = 42000 container_2.save() username, password, user = create_user() - user.user_permissions.add(Permission.objects.get(codename="view_warehouse")) + + profile_type = ProfileType.objects.create( + label="xxCollaborateur", + txt_idx="xxcollaborator", + ) + gp = Group.objects.create(name="xxLieu de conservation : voir") + gp.permissions.add(Permission.objects.get(codename="view_warehouse")) + gp.permissions.add(Permission.objects.get(codename="view_own_container")) + profile_type.groups.add(gp) + UserProfile.objects.create( + profile_type=profile_type, + person=user.ishtaruser.person, + current=True, + ) + user.ishtaruser.generate_permission() + client = Client() client.login(username=username, password=password) url = "/autocomplete-container/{}/".format(self.main_warehouse.pk) diff --git a/ishtar_common/admin.py b/ishtar_common/admin.py index f67a99e01..743d643a3 100644 --- a/ishtar_common/admin.py +++ b/ishtar_common/admin.py @@ -1655,6 +1655,9 @@ class ProfileTypeAdmin(GeneralTypeAdmin): return Http404() permissions_needed = set() permissions_not_needed = set() + for model in ("basefind", "import", "biographicalnote"): + for perm_type in ("add", "change", "delete", "view"): + permissions_not_needed.add((perm_type, model)) for group in obj.groups.all(): for perm in group.permissions.all(): sp = perm.codename.split("_") diff --git a/ishtar_common/models.py b/ishtar_common/models.py index 8fcb9edbb..0c92f2f10 100644 --- a/ishtar_common/models.py +++ b/ishtar_common/models.py @@ -27,6 +27,7 @@ from bs4 import BeautifulSoup import bleach import copy import datetime +from guardian.models import UserObjectPermission import inspect from importlib import import_module from jinja2 import TemplateSyntaxError, UndefinedError @@ -297,7 +298,7 @@ class ValueGetter: def _get_values_update_sub_filter(self, filtr, prefix): if not filtr: return - return [k[len(prefix) :] for k in filtr if k.startswith(prefix)] + return [k[len(prefix):] for k in filtr if k.startswith(prefix)] def get_extra_values(self, prefix="", no_values=False, filtr=None, **kwargs): return {} @@ -3540,6 +3541,81 @@ class UserProfile(models.Model): new_item.external_sources.add(src) return new_item + def _generate_permission(self, ishtar_user, content_type, permission_request): + item_ids = [] + model_class = content_type.model_class() + if permission_request.include_associated_items: + item_ids += model_class.filter( + ishtar_users__pk=ishtar_user.pk + ).values_list("pk", flat=True) + if permission_request.include_upstream_items: + # TODO.... + item_ids += model_class.get_ids_from_upper_permissions(ishtar_user.user_ptr.pk) + if permission_request.request or permission_request.limit_to_attached_areas: + # TODO + pass + query = model_class.objects + return item_ids + + def generate_permission(self, content_type): + ishtar_user = self.person.ishtaruser + + # add base permissions + for group in self.profile_type.groups.all(): + for perm in group.permissions.all(): + ishtar_user.user_ptr.user_permissions.add(perm) + q_has_perm = self.profile_type.groups.filter( + permissions__content_type=content_type, + permissions__codename__contains="_own_" + ) + if not q_has_perm.count(): # no permission to generate + return + + permissions = [] + for group in q_has_perm.all(): + permissions += list(group.permissions.values_list("pk", flat=True)) + q_req = self.profile_type.permission_requests.filter( + model=content_type, active=True + ) + 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") + """ + model_class = content_type.model_class() + query = model_class.get_owns(user=ishtar_user, query=True, no_auth_check=True) + if query: + item_ids = list( + model_class.objects.filter(query).values_list("pk", flat=True) + ) + else: + for perm_request in q_req.all(): + item_ids += self._generate_permission( + ishtar_user, content_type, perm_request + ) + user_id = ishtar_user.user_ptr.pk + object_permissions = [] + item_ids = list(set(item_ids)) + permissions = list(set(permissions)) + for permission_id in permissions: + exclude = list(UserObjectPermission.objects.filter( + content_type_id=content_type.pk, permission_id=permission_id, + user_id=user_id + ).values_list("object_pk", flat=True)) + object_permissions += [ + UserObjectPermission( + object_pk=str(item_id), + content_type_id=content_type.pk, + permission_id=permission_id, + user_id=user_id + ) + for item_id in item_ids if str(item_id) not in exclude + ] + if object_permissions: + UserObjectPermission.objects.bulk_create(object_permissions) + def save( self, force_insert=False, @@ -3692,8 +3768,8 @@ class IshtarUser(FullSearch): _("Advanced shortcut menu"), default=False ) # latest news read by the user - latest_news_version = models.CharField(_("Latest news version"), default="", blank=True, - max_length=20) + latest_news_version = models.CharField(_("Latest news version"), default="", + blank=True, max_length=20) display_news = models.BooleanField(_("Display news"), default=True) display_forum_entries = models.BooleanField(_("Display forum entries"), default=True) @@ -3787,32 +3863,43 @@ class IshtarUser(FullSearch): if obj: return self.user_ptr.has_perm(permission, obj) return self.user_ptr.has_perm(permission) - """ - res = ( - bool( - self.profiles.filter( - current=True, profile_type__txt_idx=permission - ).count() - ) - or bool( - self.profiles.filter( - current=True, - profile_type__groups__permissions__codename=permission, - ).count() - ) - or bool( - self.ishtaruser.user_ptr.groups.filter( - permissions__codename__in=[permission] - ).count() - ) - or bool( - self.ishtaruser.user_ptr.user_permissions.filter( - codename__in=[permission] - ).count() - ) - ) - return res - """ + + def generate_permission(self): + # models to treat first in this order to manage cascade permissions + model_names = [ + ("archaeological_operations", "operation"), + ("archaeological_context_records", "contextrecord"), + ("archaeological_warehouse", "warehouse"), + ("archaeological_finds", "treatment"), + ("archaeological_warehouse", "container"), + ("archaeological_finds", "find"), + ] + # cascade permission to treat at the end + last_model_names = [ + ("ishtar_common", "document"), + ("ishtar_common", "geovectordata"), + ("ishtar_common", "import"), + ] + for ct in ContentType.objects.all(): + name = (ct.app_label, ct.model) + klass = ct.model_class() + if not klass or not getattr(klass, "SHOW_URL", None) \ + or name in model_names \ + or name in last_model_names: + continue + model_names.append(name) + model_names += last_model_names + content_types = [ + ContentType.objects.get(app_label=app, model=model_name) + for app, model_name in model_names + ] + # clean all permission first + UserObjectPermission.objects.filter(user_id=self.pk).delete() + self.user_ptr.user_permissions.clear() + + for ct in content_types: + for profile in self.person.profiles.all(): + profile.generate_permission(ct) def full_label(self): return self.person.full_label() @@ -5014,7 +5101,14 @@ class Document( def get_query_owns(cls, ishtaruser): query_own_list = [] for rel_model in cls.RELATED_MODELS: - klass = getattr(cls, rel_model).remote_field.related_model + r = getattr(cls, rel_model) + if hasattr(r, "remote_field"): + r = getattr(cls, rel_model).remote_field + else: + r = getattr(cls, rel_model).rel + klass = r.related_model + if not hasattr(klass, "_get_query_owns_dicts"): + continue q_own_dct = klass._get_query_owns_dicts(ishtaruser) if q_own_dct: query_own_list.append((rel_model + "__", q_own_dct)) diff --git a/ishtar_common/models_common.py b/ishtar_common/models_common.py index e92d7d55d..28f5aba00 100644 --- a/ishtar_common/models_common.py +++ b/ishtar_common/models_common.py @@ -1629,13 +1629,14 @@ class BaseHistorizedItem( ) } - class Meta: - abstract = True - @classmethod def get_verbose_name(cls): return cls._meta.verbose_name + @classmethod + def get_ids_from_upper_permissions(cls, user_id): + return [] + def is_locked(self, user=None): if not user: return self.locked diff --git a/ishtar_common/utils.py b/ishtar_common/utils.py index 8de745874..09c470823 100644 --- a/ishtar_common/utils.py +++ b/ishtar_common/utils.py @@ -395,7 +395,8 @@ class OwnPerms: """ Check if the current object is owned by the user """ - print("ishtar_common/utils.py - 370 - DELETE") + print("ishtar_common/utils.py - 377 - DELETE") + raise IshtarUser = apps.get_model("ishtar_common", "IshtarUser") if isinstance(user, IshtarUser): ishtaruser = user @@ -467,13 +468,18 @@ class OwnPerms: values=None, get_short_menu_class=False, menu_filtr=None, + no_auth_check=False, + query=False ): """ Get Own items """ + return_query = query + query = None if not replace_query: replace_query = {} - if hasattr(user, "is_authenticated") and not user.is_authenticated: + if hasattr(user, "is_authenticated") and not user.is_authenticated \ + and not no_auth_check: returned = cls.objects.filter(pk__isnull=True) if values: returned = [] @@ -502,6 +508,10 @@ class OwnPerms: if values: returned = [] return returned + if return_query: + if query: + return query + return replace_query if query: q = cls.objects.filter(query) else: # replace_query diff --git a/ishtar_common/views_item.py b/ishtar_common/views_item.py index a88cb48b3..64dee8872 100644 --- a/ishtar_common/views_item.py +++ b/ishtar_common/views_item.py @@ -12,7 +12,6 @@ import requests # nosec: no user input used import subprocess # nosec from tempfile import NamedTemporaryFile -import unidecode from django.conf import settings from django.contrib.auth.models import Permission diff --git a/ishtar_common/wizards.py b/ishtar_common/wizards.py index e41c4c811..1da9b9f4d 100644 --- a/ishtar_common/wizards.py +++ b/ishtar_common/wizards.py @@ -35,7 +35,7 @@ from formtools.wizard.views import ( from django.contrib.sites.models import Site from django.core.exceptions import ObjectDoesNotExist from django.core.files.images import ImageFile -from django.core.files.storage import default_storage, FileSystemStorage +from django.core.files.storage import FileSystemStorage from django.core.mail import send_mail from django.db.models.fields.files import FileField, ImageFieldFile from django.db.models.fields.related import ManyToManyField @@ -50,7 +50,7 @@ from django.utils.safestring import mark_safe from ishtar_common import models, models_rest from ishtar_common.forms import CustomForm, reverse_lazy -from ishtar_common.utils import get_all_field_names, get_person_gdpr_log, MultiValueDict, \ +from ishtar_common.utils import get_all_field_names, get_person_gdpr_log, MultiValueDict,\ put_session_message logger = logging.getLogger(__name__) @@ -154,6 +154,7 @@ class Wizard(IshtarWizard): label = "" translated_keys = [] modification = None # True when the wizard modify an item + deletion = True # True on deletion storage_name = "formtools.wizard.storage.session.SessionStorage" wizard_done_template = "ishtar/wizard/wizard_done.html" wizard_done_window = "" @@ -211,16 +212,9 @@ class Wizard(IshtarWizard): self.steps = StepsHelper(self) current_object = self.get_current_object() - ishtaruser = ( - request.user.ishtaruser if hasattr(request.user, "ishtaruser") else None - ) - # not the first step and current object is not owned if self.steps and self.steps.first != step and current_object: - is_own = current_object.is_own( - ishtaruser, alt_query_own=self.alt_is_own_method - ) - if not is_own: + if not self.verify_permission(request, current_object): messages.add_message( request, messages.WARNING, @@ -230,6 +224,23 @@ class Wizard(IshtarWizard): return return True + def verify_permission(self, request, current_object=None): + meta = self.model._meta + perm = f"{meta.app_label}." + if self.modification: + perm += "change" + elif self.deletion: + perm += "delete" + else: + perm += "add" + base_perm = f"{perm}_{meta.model_name}" + if request.user.has_perm(base_perm): + return True + if not current_object: + return False + own_perm = f"{perm}_own_{meta.model_name}" + return request.user.has_perm(own_perm, current_object) + def dispatch(self, request, *args, **kwargs): self.current_right = kwargs.get("current_right", None) step = kwargs.get("step", None) @@ -241,7 +252,6 @@ class Wizard(IshtarWizard): self.filter_owns_items = True else: self.filter_owns_items = False - return super(Wizard, self).dispatch(request, *args, **kwargs) def get_prefix(self, request, *args, **kwargs): @@ -1714,6 +1724,8 @@ class DocumentSearch(SearchWizard): class DeletionWizard(Wizard): + deletion = True + def __init__(self, *args, **kwargs): if (not hasattr(self, "fields") or not self.fields) and ( hasattr(self, "model") and hasattr(self.model, "TABLE_COLS") @@ -1790,6 +1802,8 @@ class MultipleItemWizard(Wizard): class MultipleDeletionWizard(MultipleItemWizard): + deletion = True + def __init__(self, *args, **kwargs): if (not hasattr(self, "fields") or not self.fields) and ( hasattr(self, "model") and hasattr(self.model, "TABLE_COLS") |