diff --git a/authentik/brands/api.py b/authentik/brands/api.py index c5960e4db..2b22a3508 100644 --- a/authentik/brands/api.py +++ b/authentik/brands/api.py @@ -54,7 +54,6 @@ class BrandSerializer(ModelSerializer): "flow_unenrollment", "flow_user_settings", "flow_device_code", - "event_retention", "web_certificate", "attributes", ] @@ -125,7 +124,6 @@ class BrandViewSet(UsedByMixin, ModelViewSet): "flow_unenrollment", "flow_user_settings", "flow_device_code", - "event_retention", "web_certificate", ] ordering = ["domain"] diff --git a/authentik/brands/migrations/0005_remove_brand_event_retention.py b/authentik/brands/migrations/0005_remove_brand_event_retention.py new file mode 100644 index 000000000..5fdbc8d1b --- /dev/null +++ b/authentik/brands/migrations/0005_remove_brand_event_retention.py @@ -0,0 +1,16 @@ +# Generated by Django 4.2.7 on 2023-12-12 06:41 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("authentik_brands", "0004_tenant_flow_device_code"), + ] + + operations = [ + migrations.RemoveField( + model_name="brand", + name="event_retention", + ), + ] diff --git a/authentik/brands/models.py b/authentik/brands/models.py index f86eddabf..268aa5c26 100644 --- a/authentik/brands/models.py +++ b/authentik/brands/models.py @@ -9,7 +9,6 @@ from structlog.stdlib import get_logger from authentik.crypto.models import CertificateKeyPair from authentik.flows.models import Flow from authentik.lib.models import SerializerModel -from authentik.lib.utils.time import timedelta_string_validator LOGGER = get_logger() @@ -51,14 +50,6 @@ class Brand(SerializerModel): Flow, null=True, on_delete=models.SET_NULL, related_name="brand_device_code" ) - event_retention = models.TextField( - default="days=365", - validators=[timedelta_string_validator], - help_text=_( - "Events will be deleted after this duration.(Format: weeks=3;days=2;hours=3,seconds=2)." - ), - ) - web_certificate = models.ForeignKey( CertificateKeyPair, null=True, diff --git a/authentik/brands/tests.py b/authentik/brands/tests.py index b7d7d62ee..4b242b068 100644 --- a/authentik/brands/tests.py +++ b/authentik/brands/tests.py @@ -64,27 +64,6 @@ class TestBrands(APITestCase): }, ) - def test_event_retention(self): - """Test brand's event retention""" - brand = Brand.objects.create( - domain="foo", - default=True, - branding_title="custom", - event_retention="weeks=3", - ) - factory = RequestFactory() - request = factory.get("/") - request.brand = brand - event = Event.new(action=EventAction.SYSTEM_EXCEPTION, message="test").from_http(request) - self.assertEqual(event.expires.day, (event.created + timedelta_from_string("weeks=3")).day) - self.assertEqual( - event.expires.month, - (event.created + timedelta_from_string("weeks=3")).month, - ) - self.assertEqual( - event.expires.year, (event.created + timedelta_from_string("weeks=3")).year - ) - def test_create_default_multiple(self): """Test attempted creation of multiple default brands""" Brand.objects.create( diff --git a/authentik/events/models.py b/authentik/events/models.py index 35d2c1cbc..2e210b9d1 100644 --- a/authentik/events/models.py +++ b/authentik/events/models.py @@ -42,6 +42,7 @@ from authentik.lib.utils.http import get_client_ip, get_http_session from authentik.lib.utils.time import timedelta_from_string from authentik.policies.models import PolicyBindingModel from authentik.stages.email.utils import TemplateEmailMessage +from authentik.tenants.models import Tenant LOGGER = get_logger() if TYPE_CHECKING: @@ -224,12 +225,14 @@ class Event(SerializerModel, ExpiringModel): if QS_QUERY in self.context["http_request"]["args"]: wrapped = self.context["http_request"]["args"][QS_QUERY] self.context["http_request"]["args"] = cleanse_dict(QueryDict(wrapped)) - if hasattr(request, "brand"): - brand: Brand = request.brand + if hasattr(request, "tenant"): + tenant: Tenant = request.tenant # Because self.created only gets set on save, we can't use it's value here # hence we set self.created to now and then use it self.created = now() - self.expires = self.created + timedelta_from_string(brand.event_retention) + self.expires = self.created + timedelta_from_string(tenant.event_retention) + if hasattr(request, "brand"): + brand: Brand = request.brand self.brand = sanitize_dict(model_to_dict(brand)) if hasattr(request, "user"): original_user = None diff --git a/authentik/tenants/api.py b/authentik/tenants/api.py index 890d46e27..1fc423c77 100644 --- a/authentik/tenants/api.py +++ b/authentik/tenants/api.py @@ -105,9 +105,10 @@ class SettingsSerializer(ModelSerializer): "default_user_change_name", "default_user_change_email", "default_user_change_username", + "event_retention", + "footer_links", "gdpr_compliance", "impersonation", - "footer_links", ] diff --git a/authentik/tenants/migrations/0001_initial.py b/authentik/tenants/migrations/0001_initial.py index d0feb55bd..eda3e7c91 100644 --- a/authentik/tenants/migrations/0001_initial.py +++ b/authentik/tenants/migrations/0001_initial.py @@ -6,6 +6,7 @@ import django.db.models.deletion import django_tenants.postgresql_backend.base from django.db import migrations, models +import authentik.lib.utils.time import authentik.tenants.models from authentik.lib.config import CONFIG @@ -22,9 +23,9 @@ def create_default_tenant(apps, schema_editor): default_user_change_name=CONFIG.get_bool("default_user_change_name", True), default_user_change_email=CONFIG.get_bool("default_user_change_email", False), default_user_change_username=CONFIG.get_bool("default_user_change_username", False), + footer_links=CONFIG.get("footer_links", default=[]), gdpr_compliance=CONFIG.get_bool("gdpr_compliance", True), impersonation=CONFIG.get_bool("impersonation", True), - footer_links=CONFIG.get("footer_links", default=[]), ) @@ -81,6 +82,22 @@ class Migration(migrations.Migration): help_text="Enable the ability for users to change their username.", ), ), + ( + "event_retention", + models.TextField( + default="days=365", + help_text="Events will be deleted after this duration.(Format: weeks=3;days=2;hours=3,seconds=2).", + validators=[authentik.lib.utils.time.timedelta_string_validator], + ), + ), + ( + "footer_links", + models.JSONField( + blank=True, + default=list, + help_text="The option configures the footer links on the flow executor pages.", + ), + ), ( "gdpr_compliance", models.BooleanField( @@ -94,14 +111,6 @@ class Migration(migrations.Migration): default=True, help_text="Globally enable/disable impersonation." ), ), - ( - "footer_links", - models.JSONField( - blank=True, - default=list, - help_text="The option configures the footer links on the flow executor pages.", - ), - ), ], options={ "verbose_name": "Tenant", diff --git a/authentik/tenants/models.py b/authentik/tenants/models.py index b1c3f3ac6..603cd22f3 100644 --- a/authentik/tenants/models.py +++ b/authentik/tenants/models.py @@ -14,6 +14,7 @@ from structlog.stdlib import get_logger from authentik.blueprints.apps import ManagedAppConfig from authentik.lib.models import SerializerModel +from authentik.lib.utils.time import timedelta_string_validator LOGGER = get_logger() @@ -57,6 +58,18 @@ class Tenant(TenantMixin, SerializerModel): default_user_change_username = models.BooleanField( help_text=_("Enable the ability for users to change their username."), default=False ) + event_retention = models.TextField( + default="days=365", + validators=[timedelta_string_validator], + help_text=_( + "Events will be deleted after this duration.(Format: weeks=3;days=2;hours=3,seconds=2)." + ), + ) + footer_links = models.JSONField( + help_text=_("The option configures the footer links on the flow executor pages."), + default=list, + blank=True, + ) gdpr_compliance = models.BooleanField( help_text=_( "When enabled, all the events caused by a user " @@ -67,11 +80,6 @@ class Tenant(TenantMixin, SerializerModel): impersonation = models.BooleanField( help_text=_("Globally enable/disable impersonation."), default=True ) - footer_links = models.JSONField( - help_text=_("The option configures the footer links on the flow executor pages."), - default=list, - blank=True, - ) def save(self, *args, **kwargs): if self.schema_name == "template": diff --git a/authentik/tenants/tests/test_event_retention.py b/authentik/tenants/tests/test_event_retention.py new file mode 100644 index 000000000..daaa908f1 --- /dev/null +++ b/authentik/tenants/tests/test_event_retention.py @@ -0,0 +1,30 @@ +"""Test event retention""" +from django.test.client import RequestFactory +from django_tenants.utils import get_public_schema_name +from rest_framework.test import APITestCase + +from authentik.events.models import Event, EventAction +from authentik.lib.utils.time import timedelta_from_string +from authentik.tenants.models import Tenant + + +class TestEventRetention(APITestCase): + """Test event retention""" + + def test_event_retention(self): + """Test brand's event retention""" + default_tenant = Tenant.objects.get(schema_name=get_public_schema_name()) + default_tenant.event_retention = "weeks=3" + default_tenant.save() + factory = RequestFactory() + request = factory.get("/") + request.tenant = default_tenant + event = Event.new(action=EventAction.SYSTEM_EXCEPTION, message="test").from_http(request) + self.assertEqual(event.expires.day, (event.created + timedelta_from_string("weeks=3")).day) + self.assertEqual( + event.expires.month, + (event.created + timedelta_from_string("weeks=3")).month, + ) + self.assertEqual( + event.expires.year, (event.created + timedelta_from_string("weeks=3")).year + ) diff --git a/blueprints/schema.json b/blueprints/schema.json index 7947e0153..aaf0d82e6 100644 --- a/blueprints/schema.json +++ b/blueprints/schema.json @@ -8560,12 +8560,6 @@ "type": "integer", "title": "Flow device code" }, - "event_retention": { - "type": "string", - "minLength": 1, - "title": "Event retention", - "description": "Events will be deleted after this duration.(Format: weeks=3;days=2;hours=3,seconds=2)." - }, "web_certificate": { "type": "integer", "title": "Web certificate", diff --git a/schema.yml b/schema.yml index d0a3e7570..b75e5556d 100644 --- a/schema.yml +++ b/schema.yml @@ -3373,10 +3373,6 @@ paths: name: domain schema: type: string - - in: query - name: event_retention - schema: - type: string - in: query name: flow_authentication schema: @@ -29844,9 +29840,6 @@ components: type: string format: uuid nullable: true - event_retention: - type: string - description: 'Events will be deleted after this duration.(Format: weeks=3;days=2;hours=3,seconds=2).' web_certificate: type: string format: uuid @@ -29902,10 +29895,6 @@ components: type: string format: uuid nullable: true - event_retention: - type: string - minLength: 1 - description: 'Events will be deleted after this duration.(Format: weeks=3;days=2;hours=3,seconds=2).' web_certificate: type: string format: uuid @@ -36804,10 +36793,6 @@ components: type: string format: uuid nullable: true - event_retention: - type: string - minLength: 1 - description: 'Events will be deleted after this duration.(Format: weeks=3;days=2;hours=3,seconds=2).' web_certificate: type: string format: uuid @@ -38740,6 +38725,15 @@ components: default_user_change_username: type: boolean description: Enable the ability for users to change their username. + event_retention: + type: string + minLength: 1 + description: 'Events will be deleted after this duration.(Format: weeks=3;days=2;hours=3,seconds=2).' + footer_links: + type: object + additionalProperties: {} + description: The option configures the footer links on the flow executor + pages. gdpr_compliance: type: boolean description: When enabled, all the events caused by a user will be deleted @@ -38747,11 +38741,6 @@ components: impersonation: type: boolean description: Globally enable/disable impersonation. - footer_links: - type: object - additionalProperties: {} - description: The option configures the footer links on the flow executor - pages. PatchedStaticDeviceRequest: type: object description: Serializer for static authenticator devices @@ -41638,6 +41627,14 @@ components: default_user_change_username: type: boolean description: Enable the ability for users to change their username. + event_retention: + type: string + description: 'Events will be deleted after this duration.(Format: weeks=3;days=2;hours=3,seconds=2).' + footer_links: + type: object + additionalProperties: {} + description: The option configures the footer links on the flow executor + pages. gdpr_compliance: type: boolean description: When enabled, all the events caused by a user will be deleted @@ -41645,11 +41642,6 @@ components: impersonation: type: boolean description: Globally enable/disable impersonation. - footer_links: - type: object - additionalProperties: {} - description: The option configures the footer links on the flow executor - pages. SettingsRequest: type: object description: Settings Serializer @@ -41667,6 +41659,15 @@ components: default_user_change_username: type: boolean description: Enable the ability for users to change their username. + event_retention: + type: string + minLength: 1 + description: 'Events will be deleted after this duration.(Format: weeks=3;days=2;hours=3,seconds=2).' + footer_links: + type: object + additionalProperties: {} + description: The option configures the footer links on the flow executor + pages. gdpr_compliance: type: boolean description: When enabled, all the events caused by a user will be deleted @@ -41674,11 +41675,6 @@ components: impersonation: type: boolean description: Globally enable/disable impersonation. - footer_links: - type: object - additionalProperties: {} - description: The option configures the footer links on the flow executor - pages. SeverityEnum: enum: - notice diff --git a/web/src/admin/admin-settings/AdminSettingsForm.ts b/web/src/admin/admin-settings/AdminSettingsForm.ts index 9bedf636a..803e64183 100644 --- a/web/src/admin/admin-settings/AdminSettingsForm.ts +++ b/web/src/admin/admin-settings/AdminSettingsForm.ts @@ -7,6 +7,7 @@ import "@goauthentik/elements/forms/FormGroup"; import "@goauthentik/elements/forms/HorizontalFormElement"; import "@goauthentik/elements/forms/Radio"; import "@goauthentik/elements/forms/SearchSelect"; +import "@goauthentik/elements/utils/TimeDeltaHelp"; import { msg } from "@lit/localize"; import { TemplateResult, html } from "lit"; @@ -120,6 +121,41 @@ export class AdminSettingsForm extends Form { help=${msg("Enable the ability for users to change their username.")} > + + ${msg("Duration after which events will be deleted from the database.")} +

+

+ ${msg( + 'When using an external logging solution for archiving, this can be set to "minutes=5".', + )} +

+

+ ${msg( + "This setting only affects new Events, as the expiration is saved per-event.", + )} +

+ `} + > +
+ + ${msg( + "This option configures the footer links on the flow executor pages. It must be a valid JSON list and can be used as follows:", + )} + [{"name": "Link Name","href":"https://goauthentik.io"}] +

+ `} + > +
{ help=${msg("Globally enable/disable impersonation.")} > - - ${msg( - "This option configures the footer links on the flow executor pages. It must be a valid JSON list and can be used as follows:", - )} - [{"name": "Link Name","href":"https://goauthentik.io"}] -

- `} - > -
`; } } diff --git a/web/src/admin/brands/BrandForm.ts b/web/src/admin/brands/BrandForm.ts index 9c42d81ea..71ec2095a 100644 --- a/web/src/admin/brands/BrandForm.ts +++ b/web/src/admin/brands/BrandForm.ts @@ -235,34 +235,6 @@ export class BrandForm extends ModelForm { certificate=${this.instance?.webCertificate} > - - -

- ${msg("Duration after which events will be deleted from the database.")} -

-

- ${msg( - 'When using an external logging solution for archiving, this can be set to "minutes=5".', - )} -

-

- ${msg( - "This setting only affects new Events, as the expiration is saved per-event.", - )} -

-

- ${msg('Format: "weeks=3;days=2;hours=3,seconds=2".')} -

-