diff --git a/contracts/admin.py b/contracts/admin.py index 1cbf1f2346eafa479828a30288ecf285023b2cde..ef8ee1950457a28a91705d435e0d6c3758811dad 100644 --- a/contracts/admin.py +++ b/contracts/admin.py @@ -173,6 +173,17 @@ class ContractAdmin(MarkdownxGuardedModelAdmin, FieldsetsInlineMixin, NestedMode for index, fieldset in enumerate(fieldsets_with_inlines) ] + def get_queryset(self, request): + queryset = super().get_queryset(request) + + if not request.user.has_perm("contracts.view_confidential"): + queryset = queryset.filter(is_public=True) + + if not request.user.has_perm("contracts.approve"): + queryset = queryset.filter(is_approved=True) + + return queryset + def save_model(self, request, obj, form, change) -> None: if obj.created_by is None: obj.created_by = request.user @@ -262,25 +273,50 @@ class SigneeAdmin(MarkdownxGuardedModelAdmin): form = SigneeAdminForm - fields = ( - "name", - "entity_type", - "address_street_with_number", - "address_district", - "address_zip", - "address_country", - "ico_number", - "load_ares_data_button", - "date_of_birth", - "department", - "role", - ) - readonly_fields = ("load_ares_data_button",) list_filter = ("entity_type",) list_display = ("name", "entity_type") + def get_fields(self, request, obj=None): + fields = [ + "name", + "entity_type", + "ico_number", + "department", + "role", + ] + + if ( + obj is None # Creating + or obj.entity_has_public_address + or request.user.has_perm("contracts.view_confidential", obj) + ): + entity_type_index = fields.index("entity_type") + + fields[entity_type_index:entity_type_index] = [ + "address_street_with_number", + "address_district", + "address_zip", + "address_country", + ] + + fields.insert( + fields.index("department") - 1, + "date_of_birth", + ) + + if ( + obj is None # Allowed to create + or request.user.has_perm("contracts.edit_signee", obj) + ): + fields.insert( + fields.index("ico_number"), + "load_ares_data_button" + ) + + return fields + def load_ares_data_button(self, obj): return format_html( '<button type="button" id="load_ares_data">Načíst data</button>' diff --git a/contracts/migrations/0013_alter_contractee_address_country_and_more.py b/contracts/migrations/0013_alter_contractee_address_country_and_more.py new file mode 100644 index 0000000000000000000000000000000000000000..e2f5af7719a7b34749d68ee6066b62eb0d3b19ed --- /dev/null +++ b/contracts/migrations/0013_alter_contractee_address_country_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 4.1.4 on 2023-03-24 10:05 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('contracts', '0012_alter_contractee_address_country_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='contractee', + name='address_country', + field=models.CharField(default='CZ', max_length=256, verbose_name='Země'), + ), + migrations.AlterField( + model_name='signee', + name='address_country', + field=models.CharField(default='CZ', max_length=256, verbose_name='Země'), + ), + ] diff --git a/oidc/auth.py b/oidc/auth.py index 6d7be2d8ecace57005ed76618db44910ab105d90..975d82757265c2d41f53ce7f6c6a345bfacc9252 100644 --- a/oidc/auth.py +++ b/oidc/auth.py @@ -1,19 +1,28 @@ +import typing import logging import jwt -from django.conf import settings from django.contrib.auth.models import Group +from django.conf import settings from pirates.auth import PiratesOIDCAuthenticationBackend logging.basicConfig(level=logging.DEBUG) class RegistryOIDCAuthenticationBackend(PiratesOIDCAuthenticationBackend): - def _assign_new_user_groups(self, user, access_token, user_groups=None) -> None: + def _assign_new_user_groups( + self, + user, + access_token: dict, + user_groups: typing.Union[None, list] = None + ) -> None: if user_groups is None: user_groups = user.groups.all() for group in access_token["groups"]: + if group.startswith("_"): + continue + group_name = f"sso_{group}" group = Group.objects.filter(name=group_name) @@ -27,9 +36,12 @@ class RegistryOIDCAuthenticationBackend(PiratesOIDCAuthenticationBackend): if group not in user_groups: user.groups.add(group) - user.save() - - def _remove_old_user_groups(self, user, access_token, user_groups=None) -> None: + def _remove_old_user_groups( + self, + user, + access_token: dict, + user_groups: typing.Union[None, list] = None + ) -> None: if user_groups is None: user_groups = user.groups.all() @@ -50,10 +62,17 @@ class RegistryOIDCAuthenticationBackend(PiratesOIDCAuthenticationBackend): user_groups = user.groups.all() self._remove_old_user_groups( - user, decoded_access_token, user_groups=user_groups + user, + decoded_access_token, + user_groups=user_groups ) self._assign_new_user_groups( - user, decoded_access_token, user_groups=user_groups + user, + decoded_access_token, + user_groups=user_groups ) + user.update_group_based_admin() + user.save(saved_by_auth=True) + return user diff --git a/run.sh b/run.sh index 416f8b5af8e41c7f70372fa9aae0ee92b8bf332b..f9c9d4f10b89680873d203d95df033ca1f59f179 100644 --- a/run.sh +++ b/run.sh @@ -3,8 +3,9 @@ # exit on error set -e -# migrate database +# Migrate database +python manage.py makemigrations # Custom Group model python manage.py migrate -# start webserver +# Start webserver exec gunicorn -c gunicorn.conf.py registry.wsgi diff --git a/users/migrations/0002_user_is_staff_based_on_group.py b/users/migrations/0002_user_is_staff_based_on_group.py new file mode 100644 index 0000000000000000000000000000000000000000..2f3b12c3a6a264e05934c2ff99d18fb5c6fece10 --- /dev/null +++ b/users/migrations/0002_user_is_staff_based_on_group.py @@ -0,0 +1,18 @@ +# Generated by Django 4.1.4 on 2023-03-24 10:54 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='is_staff_based_on_group', + field=models.BooleanField(default=True, verbose_name='Admin přístup dle členství ve skupině'), + ), + ] diff --git a/users/migrations/0003_alter_user_is_staff_based_on_group.py b/users/migrations/0003_alter_user_is_staff_based_on_group.py new file mode 100644 index 0000000000000000000000000000000000000000..e08a2ff2bcb09d5c658a728a7894c8f868f3b3b3 --- /dev/null +++ b/users/migrations/0003_alter_user_is_staff_based_on_group.py @@ -0,0 +1,18 @@ +# Generated by Django 4.1.4 on 2023-03-24 10:59 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0002_user_is_staff_based_on_group'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='is_staff_based_on_group', + field=models.BooleanField(default=True, help_text='Určuje, zda bude "Administrační přístup" uživatele definován dle členství ve skupinách, nebo podle speciálního nastavení zde.', verbose_name='Administrační přístup dle členství ve skupině'), + ), + ] diff --git a/users/models.py b/users/models.py index fa51eb64eaa82299528a58a085c212c5eb49614a..6683ec6c2ce806f900bda8f1cbf2c1b523711db0 100644 --- a/users/models.py +++ b/users/models.py @@ -1,7 +1,19 @@ +from django.db import models +from django.contrib.auth.models import Group from pirates import models as pirates_models class User(pirates_models.AbstractUser): + is_staff_based_on_group = models.BooleanField( + default=True, + verbose_name="Administrační přístup dle členství ve skupině", + help_text=( + "Určuje, zda bude \"Administrační přístup\" uživatele " + "definován dle členství ve skupinách, nebo podle " + "speciálního nastavení zde." + ) + ) + def set_unusable_password(self) -> None: # Purely for compatibility with Guardian pass @@ -14,19 +26,65 @@ class User(pirates_models.AbstractUser): return f"{first_name}{self.last_name}" + # https://docs.djangoproject.com/en/4.1/ref/models/instances/#customizing-model-loading + @classmethod + def from_db(cls, db, field_names, values): + # Default implementation of from_db() (subject to change and could + # be replaced with super()). + if len(values) != len(cls._meta.concrete_fields): + values = list(values) + values.reverse() + values = [ + values.pop() if f.attname in field_names else models.DEFERRED + for f in cls._meta.concrete_fields + ] + + instance = cls(*values) + instance._state.adding = False + instance._state.db = db + + # customization to store the original field values on the instance + instance._loaded_values = dict( + zip( + field_names, + ( + value + for value in values + if value is not models.DEFERRED + ) + ) + ) + + return instance + + def save(self, *args, saved_by_auth: bool = False, **kwargs): + if ( + not self._state.adding + and not saved_by_auth + and self._loaded_values["is_staff"] != self.is_staff + ): + self.is_staff_based_on_group = False + + return super().save(*args, **kwargs) + + def update_group_based_admin(self) -> None: + if not self.is_staff_based_on_group: + return + + self.is_staff_based_on_group = True + self.is_staff = self.groups.filter(is_staff=True).exists() + @property def can_approve_contracts(self) -> bool: - # TODO: Do we need the superuser check? - return self.is_superuser or self.has_perm("contracts.approve") + return self.has_perm("contracts.approve") @property def can_create_contracts(self) -> bool: - # TODO: Do we need the superuser check? - return self.is_superuser or self.has_perm("contracts.add") + return self.has_perm("contracts.add") @property def can_view_confidential(self) -> bool: - return self.is_superuser or self.has_perm("contracts.view_confidential") + return self.has_perm("contracts.view_confidential") @property def contracts_to_approve_count(self) -> int: @@ -41,3 +99,12 @@ class User(pirates_models.AbstractUser): app_label = "users" verbose_name = "Uživatel" verbose_name_plural = "Uživatelé" + + +if not hasattr(Group, "is_staff"): + is_staff = models.BooleanField( + default=False, + verbose_name="Administrační přístup", + help_text="Určuje, zda se skupina může přihlásit do správy tohoto webu.", + ) + is_staff.contribute_to_class(Group, "is_staff")