diff --git a/.env.kesaseteli.example b/.env.kesaseteli.example index 19a847e000..972d56c5f6 100644 --- a/.env.kesaseteli.example +++ b/.env.kesaseteli.example @@ -105,4 +105,5 @@ ENABLE_ADMIN=1 PASSWORD_LOGIN_DISABLED=0 CREATE_SUMMERVOUCHER_CONFIGURATION_2026=1 CREATE_SUMMERVOUCHER_CONFIGURATION_CURRENT_YEAR=1 -ENSURE_EMAIL_TEMPLATES=1 \ No newline at end of file +ENSURE_EMAIL_TEMPLATES=1 +AD_ADMIN_GROUP_NAME= diff --git a/backend/kesaseteli/README.md b/backend/kesaseteli/README.md index 492b0d9caa..a8573bb314 100644 --- a/backend/kesaseteli/README.md +++ b/backend/kesaseteli/README.md @@ -107,6 +107,7 @@ env variables / settings are provided by Azure blob storage: | `NEXT_PUBLIC_DISABLE_VTJ` | A boolean value. If set to True, VTJ client usage is disabled. Default is False. | | `CREATE_SUMMERVOUCHER_CONFIGURATION_CURRENT_YEAR` | A boolean value. If set to True, creates a new `SummerVoucherConfiguration` for the current year. Default is False. | | `CREATE_SUMMERVOUCHER_CONFIGURATION_2026` | A boolean value. If set to True, creates a new `SummerVoucherConfiguration` for the year 2026. Default is False. | +| `AD_ADMIN_GROUP_NAME` | The name of the AD group that maps to Django admin permissions. Default is None (feature disabled). | ## Documentation diff --git a/backend/kesaseteli/applications/exporters/excel_exporter.py b/backend/kesaseteli/applications/exporters/excel_exporter.py index 226f2e1d5d..8f516389c8 100644 --- a/backend/kesaseteli/applications/exporters/excel_exporter.py +++ b/backend/kesaseteli/applications/exporters/excel_exporter.py @@ -365,9 +365,7 @@ def generate_data_row( elif field.title == RECEIVED_DATE_FIELD_TITLE: submitted_at = getattr(summer_voucher, "submitted_at", None) cell_value = ( - submitted_at.astimezone().strftime("%d/%m/%Y") - if submitted_at - else "" + submitted_at.astimezone().strftime("%d/%m/%Y") if submitted_at else "" ) else: attr_names = field.model_fields diff --git a/backend/kesaseteli/applications/migrations/0038_summervoucherconfiguration_and_more.py b/backend/kesaseteli/applications/migrations/0038_summervoucherconfiguration_and_more.py index c0d6b8ce6c..4bc54c2490 100644 --- a/backend/kesaseteli/applications/migrations/0038_summervoucherconfiguration_and_more.py +++ b/backend/kesaseteli/applications/migrations/0038_summervoucherconfiguration_and_more.py @@ -8,48 +8,145 @@ class Migration(migrations.Migration): - dependencies = [ - ('applications', '0037_set_year_2026_schools'), + ("applications", "0037_set_year_2026_schools"), ] operations = [ migrations.CreateModel( - name='SummerVoucherConfiguration', + name="SummerVoucherConfiguration", fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='time created')), - ('modified_at', models.DateTimeField(auto_now=True, verbose_name='time modified')), - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('year', models.IntegerField(unique=True, validators=[django.core.validators.MinValueValidator(2020)], verbose_name='year')), - ('voucher_value_in_euros', models.PositiveIntegerField(verbose_name='voucher value in euros')), - ('min_work_compensation_in_euros', models.PositiveIntegerField(verbose_name='minimum work compensation in euros')), - ('min_work_hours', models.PositiveIntegerField(verbose_name='minimum work hours')), - ('target_group', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(choices=[('primary_target_group', '9. luokkalainen'), ('secondary_target_group', 'Toisen asteen ensimmäisen vuoden opiskelija')], max_length=256), size=None, verbose_name='target group')), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, verbose_name="time created" + ), + ), + ( + "modified_at", + models.DateTimeField(auto_now=True, verbose_name="time modified"), + ), + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ( + "year", + models.IntegerField( + unique=True, + validators=[django.core.validators.MinValueValidator(2020)], + verbose_name="year", + ), + ), + ( + "voucher_value_in_euros", + models.PositiveIntegerField(verbose_name="voucher value in euros"), + ), + ( + "min_work_compensation_in_euros", + models.PositiveIntegerField( + verbose_name="minimum work compensation in euros" + ), + ), + ( + "min_work_hours", + models.PositiveIntegerField(verbose_name="minimum work hours"), + ), + ( + "target_group", + django.contrib.postgres.fields.ArrayField( + base_field=models.CharField( + choices=[ + ("primary_target_group", "9. luokkalainen"), + ( + "secondary_target_group", + "Toisen asteen ensimmäisen vuoden opiskelija", + ), + ], + max_length=256, + ), + size=None, + verbose_name="target group", + ), + ), ], options={ - 'verbose_name': 'summer voucher configuration', - 'verbose_name_plural': 'summer voucher configurations', - 'ordering': ['-year'], + "verbose_name": "summer voucher configuration", + "verbose_name_plural": "summer voucher configurations", + "ordering": ["-year"], }, ), migrations.AlterField( - model_name='employersummervoucher', - name='target_group', - field=models.CharField(blank=True, choices=[('primary_target_group', '9. luokkalainen'), ('secondary_target_group', 'Toisen asteen ensimmäisen vuoden opiskelija')], help_text="Summer voucher's target group type", max_length=256, verbose_name='summer voucher target group'), + model_name="employersummervoucher", + name="target_group", + field=models.CharField( + blank=True, + choices=[ + ("primary_target_group", "9. luokkalainen"), + ( + "secondary_target_group", + "Toisen asteen ensimmäisen vuoden opiskelija", + ), + ], + help_text="Summer voucher's target group type", + max_length=256, + verbose_name="summer voucher target group", + ), ), migrations.AlterField( - model_name='historicalemployersummervoucher', - name='target_group', - field=models.CharField(blank=True, choices=[('primary_target_group', '9. luokkalainen'), ('secondary_target_group', 'Toisen asteen ensimmäisen vuoden opiskelija')], help_text="Summer voucher's target group type", max_length=256, verbose_name='summer voucher target group'), + model_name="historicalemployersummervoucher", + name="target_group", + field=models.CharField( + blank=True, + choices=[ + ("primary_target_group", "9. luokkalainen"), + ( + "secondary_target_group", + "Toisen asteen ensimmäisen vuoden opiskelija", + ), + ], + help_text="Summer voucher's target group type", + max_length=256, + verbose_name="summer voucher target group", + ), ), migrations.AlterField( - model_name='historicalyouthsummervoucher', - name='target_group', - field=models.CharField(blank=True, choices=[('primary_target_group', '9. luokkalainen'), ('secondary_target_group', 'Toisen asteen ensimmäisen vuoden opiskelija')], help_text="Summer voucher's target group type", max_length=256, verbose_name='summer voucher target group'), + model_name="historicalyouthsummervoucher", + name="target_group", + field=models.CharField( + blank=True, + choices=[ + ("primary_target_group", "9. luokkalainen"), + ( + "secondary_target_group", + "Toisen asteen ensimmäisen vuoden opiskelija", + ), + ], + help_text="Summer voucher's target group type", + max_length=256, + verbose_name="summer voucher target group", + ), ), migrations.AlterField( - model_name='youthsummervoucher', - name='target_group', - field=models.CharField(blank=True, choices=[('primary_target_group', '9. luokkalainen'), ('secondary_target_group', 'Toisen asteen ensimmäisen vuoden opiskelija')], help_text="Summer voucher's target group type", max_length=256, verbose_name='summer voucher target group'), + model_name="youthsummervoucher", + name="target_group", + field=models.CharField( + blank=True, + choices=[ + ("primary_target_group", "9. luokkalainen"), + ( + "secondary_target_group", + "Toisen asteen ensimmäisen vuoden opiskelija", + ), + ], + help_text="Summer voucher's target group type", + max_length=256, + verbose_name="summer voucher target group", + ), ), ] diff --git a/backend/kesaseteli/applications/migrations/0039_emailtemplate.py b/backend/kesaseteli/applications/migrations/0039_emailtemplate.py index cc0b242d88..8298389a31 100644 --- a/backend/kesaseteli/applications/migrations/0039_emailtemplate.py +++ b/backend/kesaseteli/applications/migrations/0039_emailtemplate.py @@ -1,34 +1,94 @@ # Generated by Django 5.1.15 on 2026-01-09 16:22 -import applications.validators import uuid + from django.db import migrations, models +import applications.validators -class Migration(migrations.Migration): +class Migration(migrations.Migration): dependencies = [ - ('applications', '0038_summervoucherconfiguration_and_more'), + ("applications", "0038_summervoucherconfiguration_and_more"), ] operations = [ migrations.CreateModel( - name='EmailTemplate', + name="EmailTemplate", fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='time created')), - ('modified_at', models.DateTimeField(auto_now=True, verbose_name='time modified')), - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('type', models.CharField(choices=[('activation', 'Activation'), ('additional_info_request', 'Additional information request'), ('processing', 'Processing'), ('youth_summer_voucher', 'Youth summer voucher')], max_length=64, verbose_name='template type')), - ('language', models.CharField(choices=[('fi', 'suomi'), ('sv', 'svenska'), ('en', 'english')], max_length=2, verbose_name='language')), - ('subject', models.CharField(max_length=255, validators=[applications.validators.validate_template_syntax], verbose_name='subject')), - ('html_body', models.TextField(validators=[applications.validators.validate_template_syntax], verbose_name='HTML body')), - ('text_body', models.TextField(blank=True, help_text='Text body will be auto-generated from HTML body.', validators=[applications.validators.validate_template_syntax], verbose_name='text body')), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, verbose_name="time created" + ), + ), + ( + "modified_at", + models.DateTimeField(auto_now=True, verbose_name="time modified"), + ), + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ( + "type", + models.CharField( + choices=[ + ("activation", "Activation"), + ( + "additional_info_request", + "Additional information request", + ), + ("processing", "Processing"), + ("youth_summer_voucher", "Youth summer voucher"), + ], + max_length=64, + verbose_name="template type", + ), + ), + ( + "language", + models.CharField( + choices=[("fi", "suomi"), ("sv", "svenska"), ("en", "english")], + max_length=2, + verbose_name="language", + ), + ), + ( + "subject", + models.CharField( + max_length=255, + validators=[applications.validators.validate_template_syntax], + verbose_name="subject", + ), + ), + ( + "html_body", + models.TextField( + validators=[applications.validators.validate_template_syntax], + verbose_name="HTML body", + ), + ), + ( + "text_body", + models.TextField( + blank=True, + help_text="Text body will be auto-generated from HTML body.", + validators=[applications.validators.validate_template_syntax], + verbose_name="text body", + ), + ), ], options={ - 'verbose_name': 'email template', - 'verbose_name_plural': 'email templates', - 'ordering': ['type', 'language'], - 'unique_together': {('type', 'language')}, + "verbose_name": "email template", + "verbose_name_plural": "email templates", + "ordering": ["type", "language"], + "unique_together": {("type", "language")}, }, ), ] diff --git a/backend/kesaseteli/applications/migrations/0040_alter_employerapplication_options_and_more.py b/backend/kesaseteli/applications/migrations/0040_alter_employerapplication_options_and_more.py index bedada1387..60111aba34 100644 --- a/backend/kesaseteli/applications/migrations/0040_alter_employerapplication_options_and_more.py +++ b/backend/kesaseteli/applications/migrations/0040_alter_employerapplication_options_and_more.py @@ -5,41 +5,76 @@ class Migration(migrations.Migration): - dependencies = [ - ('applications', '0039_emailtemplate'), + ("applications", "0039_emailtemplate"), ] operations = [ migrations.AlterModelOptions( - name='employerapplication', - options={'ordering': ['-created_at'], 'verbose_name': 'employer application', 'verbose_name_plural': 'employer applications'}, + name="employerapplication", + options={ + "ordering": ["-created_at"], + "verbose_name": "employer application", + "verbose_name_plural": "employer applications", + }, ), migrations.AlterModelOptions( - name='employersummervoucher', - options={'ordering': ['-application__created_at', 'ordering'], 'verbose_name': 'employer summer voucher', 'verbose_name_plural': 'employer summer vouchers'}, + name="employersummervoucher", + options={ + "ordering": ["-application__created_at", "ordering"], + "verbose_name": "employer summer voucher", + "verbose_name_plural": "employer summer vouchers", + }, ), migrations.AlterModelOptions( - name='historicalemployerapplication', - options={'get_latest_by': ('history_date', 'history_id'), 'ordering': ('-history_date', '-history_id'), 'verbose_name': 'historical employer application', 'verbose_name_plural': 'historical employer applications'}, + name="historicalemployerapplication", + options={ + "get_latest_by": ("history_date", "history_id"), + "ordering": ("-history_date", "-history_id"), + "verbose_name": "historical employer application", + "verbose_name_plural": "historical employer applications", + }, ), migrations.AlterModelOptions( - name='historicalemployersummervoucher', - options={'get_latest_by': ('history_date', 'history_id'), 'ordering': ('-history_date', '-history_id'), 'verbose_name': 'historical employer summer voucher', 'verbose_name_plural': 'historical employer summer vouchers'}, + name="historicalemployersummervoucher", + options={ + "get_latest_by": ("history_date", "history_id"), + "ordering": ("-history_date", "-history_id"), + "verbose_name": "historical employer summer voucher", + "verbose_name_plural": "historical employer summer vouchers", + }, ), migrations.AlterField( - model_name='attachment', - name='summer_voucher', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='attachments', to='applications.employersummervoucher', verbose_name='employer summer voucher'), + model_name="attachment", + name="summer_voucher", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="attachments", + to="applications.employersummervoucher", + verbose_name="employer summer voucher", + ), ), migrations.AlterField( - model_name='employersummervoucher', - name='application', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='summer_vouchers', to='applications.employerapplication', verbose_name='employer application'), + model_name="employersummervoucher", + name="application", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="summer_vouchers", + to="applications.employerapplication", + verbose_name="employer application", + ), ), migrations.AlterField( - model_name='historicalemployersummervoucher', - name='application', - field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='applications.employerapplication', verbose_name='employer application'), + model_name="historicalemployersummervoucher", + name="application", + field=models.ForeignKey( + blank=True, + db_constraint=False, + null=True, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + to="applications.employerapplication", + verbose_name="employer application", + ), ), ] diff --git a/backend/kesaseteli/companies/migrations/0006_alter_company_options.py b/backend/kesaseteli/companies/migrations/0006_alter_company_options.py index 62fb141097..f251c89134 100644 --- a/backend/kesaseteli/companies/migrations/0006_alter_company_options.py +++ b/backend/kesaseteli/companies/migrations/0006_alter_company_options.py @@ -4,14 +4,17 @@ class Migration(migrations.Migration): - dependencies = [ - ('companies', '0005_remove_company_eauth_profile'), + ("companies", "0005_remove_company_eauth_profile"), ] operations = [ migrations.AlterModelOptions( - name='company', - options={'ordering': ['name'], 'verbose_name': 'company', 'verbose_name_plural': 'companies'}, + name="company", + options={ + "ordering": ["name"], + "verbose_name": "company", + "verbose_name_plural": "companies", + }, ), ] diff --git a/backend/kesaseteli/kesaseteli/settings.py b/backend/kesaseteli/kesaseteli/settings.py index fc3498e67b..90cbe34a73 100644 --- a/backend/kesaseteli/kesaseteli/settings.py +++ b/backend/kesaseteli/kesaseteli/settings.py @@ -104,6 +104,8 @@ CLEAR_AUDIT_LOG_ENTRIES=(bool, False), ENABLE_SEND_AUDIT_LOG=(bool, False), ENABLE_ADMIN=(bool, False), + # Configuration for the staff admin group name (ADFS group). Default is None. + AD_ADMIN_GROUP_NAME=(str, None), PASSWORD_LOGIN_DISABLED=(bool, False), DB_PREFIX=(str, ""), EMAIL_USE_TLS=(bool, False), @@ -156,6 +158,7 @@ ENCRYPTION_KEY = env.str("ENCRYPTION_KEY") SOCIAL_SECURITY_NUMBER_HASH_KEY = env.str("SOCIAL_SECURITY_NUMBER_HASH_KEY") ENABLE_ADMIN = env.bool("ENABLE_ADMIN") +AD_ADMIN_GROUP_NAME = env.str("AD_ADMIN_GROUP_NAME") PASSWORD_LOGIN_DISABLED = env.bool("PASSWORD_LOGIN_DISABLED") NEXT_PUBLIC_DISABLE_VTJ = env.bool("NEXT_PUBLIC_DISABLE_VTJ") VTJ_PERSONAL_ID_QUERY_URL = env.str("VTJ_PERSONAL_ID_QUERY_URL") @@ -697,6 +700,7 @@ "AUTO_ASSIGN_ADMIN_TO_STAFF", default=DEBUG and NEXT_PUBLIC_MOCK_FLAG ) + # Summer Voucher default / fallback configurations SUMMER_VOUCHER_DEFAULT_VOUCHER_VALUE = 350 SUMMER_VOUCHER_DEFAULT_MIN_WORK_COMPENSATION = 500 diff --git a/backend/kesaseteli/staff_admin_permissions/README.md b/backend/kesaseteli/staff_admin_permissions/README.md index ede2f4acb3..d9d450b279 100644 --- a/backend/kesaseteli/staff_admin_permissions/README.md +++ b/backend/kesaseteli/staff_admin_permissions/README.md @@ -1,11 +1,11 @@ # Staff Admin Permissions -This app provides utilities to automatically manage an "admins" user group and simplify permission setup for staff users in **development environments**. +This app provides utilities to automatically manage a configurable admin user group (default `None`) and simplify permission setup for staff users in **development environments**. ## Purpose The main goals are: -1. To ensure the `admins` group is created during the `post_migrate` process in all environments. +1. To ensure the admin group (configured via `AD_ADMIN_GROUP_NAME`) is created during the `post_migrate` process in all environments. 2. To maintain full CRUD (Create, Read, Update, Delete) permissions for all models registered in the Django Admin in local development. 3. To automatically assign this group to new staff users (e.g., mock users) during local development to avoid manual configuration. @@ -13,7 +13,7 @@ The main goals are: ### `setup_admin_permissions` -This command is responsible for calculating and assigning permissions to the `admins` group. +This command is responsible for calculating and assigning permissions to the admin group. **Usage:** ```bash @@ -21,24 +21,24 @@ python manage.py setup_admin_permissions ``` **What it does:** -1. Creates or retrieves the `admins` group. +1. Creates or retrieves the admin group (default `None`). 2. Scans the Django Admin registry (`admin.site._registry`) to find all registered models. 3. Collects `add`, `change`, `delete`, and `view` permissions for those models. -4. Assigns these permissions to the `admins` group. +4. Assigns these permissions to the admin group. **When to run:** -- Run this manually whenever you register new models in the Django Admin or need to refresh the permissions for the `admins` group. +- Run this manually whenever you register new models in the Django Admin or need to refresh the permissions for the admin group. ## Signals ### 1. `initialize_admins_group` (`post_migrate`) - **Trigger**: Runs automatically after every `migrate`. -- **Action**: Checks if the `admins` group exists. If not, it creates it. +- **Action**: Checks if the admin group exists. If not, it creates it. - **Note**: It does **not** assign permissions. This ensures the group exists so that automated tests or signals don't fail, but avoids the overhead of permissions calculation during every migration. ### 2. `assign_admins_group` (`post_save` on `User`) - **Trigger**: Runs whenever a `User` is saved. -- **Action**: Checks if the user is newly created and has `is_staff=True`. If so, it adds them to the `admins` group. +- **Action**: Checks if the user is newly created and has `is_staff=True`. If so, it adds them to the admin group (default `None`). ## Security Restrictions @@ -47,5 +47,5 @@ python manage.py setup_admin_permissions The `assign_admins_group` signal has a strict security check: -- It **ONLY** adds the user to the `admins` group if `settings.AUTO_ASSIGN_ADMIN_TO_STAFF` is `True`. +- It **ONLY** adds the user to the admin group if `settings.AUTO_ASSIGN_ADMIN_TO_STAFF` is `True`. - This creates a safeguard to prevent staff users from automatically gaining broad administrative privileges in production environments. diff --git a/backend/kesaseteli/staff_admin_permissions/constants.py b/backend/kesaseteli/staff_admin_permissions/constants.py deleted file mode 100644 index d105cd2cef..0000000000 --- a/backend/kesaseteli/staff_admin_permissions/constants.py +++ /dev/null @@ -1 +0,0 @@ -ADMIN_GROUP_NAME = "admins" diff --git a/backend/kesaseteli/staff_admin_permissions/management/commands/setup_admin_permissions.py b/backend/kesaseteli/staff_admin_permissions/management/commands/setup_admin_permissions.py index dc1e178810..6d6fc0f2d3 100644 --- a/backend/kesaseteli/staff_admin_permissions/management/commands/setup_admin_permissions.py +++ b/backend/kesaseteli/staff_admin_permissions/management/commands/setup_admin_permissions.py @@ -1,13 +1,12 @@ import logging from django.apps import apps +from django.conf import settings from django.contrib import admin from django.contrib.auth.models import Group, Permission from django.core.management.base import BaseCommand from django.db import transaction -from staff_admin_permissions.constants import ADMIN_GROUP_NAME - LOGGER = logging.getLogger(__name__) @@ -18,14 +17,24 @@ class Command(BaseCommand): ) def handle(self, *args, **options): + if not settings.AD_ADMIN_GROUP_NAME: + self.stdout.write( + self.style.WARNING( + "AD_ADMIN_GROUP_NAME is not set. Skipping admin group setup." + ) + ) + return + with transaction.atomic(): - group, created = Group.objects.get_or_create(name=ADMIN_GROUP_NAME) + group, created = Group.objects.get_or_create( + name=settings.AD_ADMIN_GROUP_NAME + ) action = "Created" if created else "Updated" if not apps.is_installed("django.contrib.admin"): msg = ( - f"{action} group '{ADMIN_GROUP_NAME}', but 'django.contrib.admin' " - "is not installed. Skipping permission setup." + f"{action} group '{settings.AD_ADMIN_GROUP_NAME}', but " + "django.contrib.admin is not installed. Skipping permission setup." ) LOGGER.warning(msg) self.stdout.write(self.style.WARNING(msg)) @@ -49,8 +58,8 @@ def handle(self, *args, **options): group.permissions.set(permissions_to_add) msg = ( - f"{action} group '{ADMIN_GROUP_NAME}' with {len(permissions_to_add)} " - "permissions." + f"{action} group '{settings.AD_ADMIN_GROUP_NAME}' with " + f"{len(permissions_to_add)} permissions." ) LOGGER.info(msg) self.stdout.write(self.style.SUCCESS(msg)) diff --git a/backend/kesaseteli/staff_admin_permissions/signals.py b/backend/kesaseteli/staff_admin_permissions/signals.py index 73bd80afdd..0ae0e102c2 100644 --- a/backend/kesaseteli/staff_admin_permissions/signals.py +++ b/backend/kesaseteli/staff_admin_permissions/signals.py @@ -3,8 +3,6 @@ from django.conf import settings from django.contrib.auth.models import Group -from staff_admin_permissions.constants import ADMIN_GROUP_NAME - LOGGER = logging.getLogger(__name__) @@ -13,9 +11,16 @@ def initialize_admins_group(sender, **kwargs): Ensure the 'admins' group exists after migration. This does NOT assign permissions, only creates the empty group if missing. """ - group, created = Group.objects.get_or_create(name=ADMIN_GROUP_NAME) + if not settings.AD_ADMIN_GROUP_NAME: + LOGGER.info( + "AD_ADMIN_GROUP_NAME is not set (None or empty). " + "Skipping creation of admin group." + ) + return + + group, created = Group.objects.get_or_create(name=settings.AD_ADMIN_GROUP_NAME) if created: - LOGGER.info(f"Created group '{ADMIN_GROUP_NAME}'.") + LOGGER.info(f"Created group '{settings.AD_ADMIN_GROUP_NAME}'.") def assign_admins_group(sender, instance, created, **kwargs): @@ -23,20 +28,30 @@ def assign_admins_group(sender, instance, created, **kwargs): Assign the admin group to newly created staff users ONLY if we are in a development environment (AUTO_ASSIGN_ADMIN_TO_STAFF is True). """ + if not settings.AD_ADMIN_GROUP_NAME: + LOGGER.info( + "AD_ADMIN_GROUP_NAME is not set (None or empty). " + "Skipping assignment of admin group." + ) + return + if created and instance.is_staff: if not settings.AUTO_ASSIGN_ADMIN_TO_STAFF: LOGGER.info( - f"Skipping auto-assignment of '{ADMIN_GROUP_NAME}' " + f"Skipping auto-assignment of '{settings.AD_ADMIN_GROUP_NAME}' " f"group for user {instance} " "because AUTO_ASSIGN_ADMIN_TO_STAFF is not enabled." ) return try: - group = Group.objects.get(name=ADMIN_GROUP_NAME) + group = Group.objects.get(name=settings.AD_ADMIN_GROUP_NAME) instance.groups.add(group) - LOGGER.info(f"Added user {instance} to '{ADMIN_GROUP_NAME}' group.") + LOGGER.info( + f"Added user {instance} to '{settings.AD_ADMIN_GROUP_NAME}' group." + ) except Group.DoesNotExist: LOGGER.warning( - f"'{ADMIN_GROUP_NAME}' group does not exist. Skipping group assignment." + f"'{settings.AD_ADMIN_GROUP_NAME}' group does not exist. " + "Skipping group assignment." ) diff --git a/backend/kesaseteli/staff_admin_permissions/tests/test_management_command.py b/backend/kesaseteli/staff_admin_permissions/tests/test_management_command.py index 2d0785cc12..e1d0a57a40 100644 --- a/backend/kesaseteli/staff_admin_permissions/tests/test_management_command.py +++ b/backend/kesaseteli/staff_admin_permissions/tests/test_management_command.py @@ -6,8 +6,6 @@ from django.contrib.contenttypes.models import ContentType from django.core.management import call_command -from staff_admin_permissions.constants import ADMIN_GROUP_NAME - @pytest.mark.django_db def test_setup_admin_permissions_command(settings): @@ -17,8 +15,9 @@ def test_setup_admin_permissions_command(settings): 2. Assigns permissions to it. """ settings.AUTO_ASSIGN_ADMIN_TO_STAFF = True + settings.AD_ADMIN_GROUP_NAME = "kesaseteli-admin" # Ensure group doesn't exist initially - Group.objects.filter(name=ADMIN_GROUP_NAME).delete() + Group.objects.filter(name=settings.AD_ADMIN_GROUP_NAME).delete() user_model = get_user_model() @@ -36,9 +35,9 @@ def test_setup_admin_permissions_command(settings): call_command("setup_admin_permissions") # Verify group exists - assert Group.objects.filter(name=ADMIN_GROUP_NAME).exists() + assert Group.objects.filter(name=settings.AD_ADMIN_GROUP_NAME).exists() - admins_group = Group.objects.get(name=ADMIN_GROUP_NAME) + admins_group = Group.objects.get(name=settings.AD_ADMIN_GROUP_NAME) # Since we mocked registry to only have User, we check User permissions assert admins_group.permissions.count() > 0 diff --git a/backend/kesaseteli/staff_admin_permissions/tests/test_signals.py b/backend/kesaseteli/staff_admin_permissions/tests/test_signals.py index 89b6afc822..72a5e8c07d 100644 --- a/backend/kesaseteli/staff_admin_permissions/tests/test_signals.py +++ b/backend/kesaseteli/staff_admin_permissions/tests/test_signals.py @@ -2,25 +2,40 @@ from django.contrib.auth import get_user_model from django.contrib.auth.models import Group -from staff_admin_permissions.constants import ADMIN_GROUP_NAME from staff_admin_permissions.signals import initialize_admins_group User = get_user_model() # noqa: N806 @pytest.mark.django_db -def test_initialize_admins_group(): +def test_initialize_admins_group(settings): """Test that the initialize signal creates the group but NOT permissions.""" - Group.objects.filter(name=ADMIN_GROUP_NAME).delete() + # settings.AD_ADMIN_GROUP_NAME defaults to None in settings.py + # but let's be explicit or use the default + group_name = "kesaseteli-admin" + settings.AD_ADMIN_GROUP_NAME = group_name + Group.objects.filter(name=group_name).delete() initialize_admins_group(sender=None) - assert Group.objects.filter(name=ADMIN_GROUP_NAME).exists() - admins_group = Group.objects.get(name=ADMIN_GROUP_NAME) + assert Group.objects.filter(name=group_name).exists() + admins_group = Group.objects.get(name=group_name) # Should be empty of permissions initially assert admins_group.permissions.count() == 0 +@pytest.mark.django_db +def test_initialize_admins_group_skips_when_setting_is_none(settings): + """Test that initialization is skipped if AD_ADMIN_GROUP_NAME is None.""" + settings.AD_ADMIN_GROUP_NAME = None + # Ensure no groups exist initially (or at least check count doesn't increase) + initial_count = Group.objects.count() + + initialize_admins_group(sender=None) + + assert Group.objects.count() == initial_count + + @pytest.mark.django_db @pytest.mark.parametrize( "auto_assign_flag,is_staff,expect_group", @@ -36,6 +51,7 @@ def test_assign_admins_group_logic(auto_assign_flag, is_staff, expect_group, set Parametrized test for 'assign_admins_group' signal logic. """ settings.AUTO_ASSIGN_ADMIN_TO_STAFF = auto_assign_flag + settings.AD_ADMIN_GROUP_NAME = "kesaseteli-admin" # Ensure group exists initialize_admins_group(sender=None) @@ -46,7 +62,7 @@ def test_assign_admins_group_logic(auto_assign_flag, is_staff, expect_group, set is_staff=is_staff, ) - admins_group = Group.objects.get(name=ADMIN_GROUP_NAME) + admins_group = Group.objects.get(name=settings.AD_ADMIN_GROUP_NAME) assert (admins_group in user.groups.all()) == expect_group @@ -56,12 +72,14 @@ def test_assign_admins_group_skips_if_group_missing(caplog, settings): Test that it handles the case gracefully if the admin group doesn't exist. """ settings.AUTO_ASSIGN_ADMIN_TO_STAFF = True + settings.AD_ADMIN_GROUP_NAME = "kesaseteli-admin" + # Ensure group does NOT exist - Group.objects.filter(name=ADMIN_GROUP_NAME).delete() + Group.objects.filter(name=settings.AD_ADMIN_GROUP_NAME).delete() # This should not raise an exception User.objects.create_user( username="test_staff_no_group", password="pw", is_staff=True ) - assert f"'{ADMIN_GROUP_NAME}' group does not exist" in caplog.text + assert f"'{settings.AD_ADMIN_GROUP_NAME}' group does not exist" in caplog.text