Compare commits
	
		
			2 Commits
		
	
	
		
			next
			...
			enterprise
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 74deedc9d4 | |||
| 3c8232b9a5 | 
							
								
								
									
										0
									
								
								authentik/enterprise/reporting/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								authentik/enterprise/reporting/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										0
									
								
								authentik/enterprise/reporting/api/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								authentik/enterprise/reporting/api/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										12
									
								
								authentik/enterprise/reporting/apps.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								authentik/enterprise/reporting/apps.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,12 @@ | |||||||
|  | """Reporting app config""" | ||||||
|  |  | ||||||
|  | from authentik.enterprise.apps import EnterpriseConfig | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class AuthentikEnterpriseReporting(EnterpriseConfig): | ||||||
|  |     """authentik enterprise reporting app config""" | ||||||
|  |  | ||||||
|  |     name = "authentik.enterprise.reporting" | ||||||
|  |     label = "authentik_reporting" | ||||||
|  |     verbose_name = "authentik Enterprise.Reporting" | ||||||
|  |     default = True | ||||||
							
								
								
									
										22
									
								
								authentik/enterprise/reporting/executor.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								authentik/enterprise/reporting/executor.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,22 @@ | |||||||
|  | from structlog.stdlib import get_logger | ||||||
|  |  | ||||||
|  | from authentik.enterprise.reporting.models import Report | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class ReportExecutor: | ||||||
|  |     """Execute a report""" | ||||||
|  |  | ||||||
|  |     def __init__(self, report: Report) -> None: | ||||||
|  |         self.report = report | ||||||
|  |         self.logger = get_logger().bind(report=self.report) | ||||||
|  |  | ||||||
|  |     def execute(self): | ||||||
|  |         # 1. Run through policies bound to report itself | ||||||
|  |         # 2. Get all bound components by running through ReportComponentBinding, | ||||||
|  |         #   while evaluating policies bound to each | ||||||
|  |         # 3. render the actual components | ||||||
|  |         # 4. Store the final data...somewhere?? | ||||||
|  |         # 5. Optionally render PDF via chromedriver (special frontend that uses API) | ||||||
|  |         #   (not required for MVP) | ||||||
|  |         # 6. Send out link to CSV/PDF or attach to email via delivery | ||||||
|  |         pass | ||||||
							
								
								
									
										131
									
								
								authentik/enterprise/reporting/migrations/0001_initial.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										131
									
								
								authentik/enterprise/reporting/migrations/0001_initial.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,131 @@ | |||||||
|  | # Generated by Django 5.0.4 on 2024-04-18 21:47 | ||||||
|  |  | ||||||
|  | import authentik.lib.models | ||||||
|  | import django.db.models.deletion | ||||||
|  | import uuid | ||||||
|  | from django.conf import settings | ||||||
|  | from django.db import migrations, models | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Migration(migrations.Migration): | ||||||
|  |  | ||||||
|  |     initial = True | ||||||
|  |  | ||||||
|  |     dependencies = [ | ||||||
|  |         ("authentik_events", "0007_event_authentik_e_action_9a9dd9_idx_and_more"), | ||||||
|  |         ("authentik_policies", "0011_policybinding_failure_result_and_more"), | ||||||
|  |         migrations.swappable_dependency(settings.AUTH_USER_MODEL), | ||||||
|  |     ] | ||||||
|  |  | ||||||
|  |     operations = [ | ||||||
|  |         migrations.CreateModel( | ||||||
|  |             name="ReportComponent", | ||||||
|  |             fields=[ | ||||||
|  |                 ( | ||||||
|  |                     "widget_uuid", | ||||||
|  |                     models.UUIDField( | ||||||
|  |                         default=uuid.uuid4, editable=False, primary_key=True, serialize=False | ||||||
|  |                     ), | ||||||
|  |                 ), | ||||||
|  |             ], | ||||||
|  |             options={ | ||||||
|  |                 "verbose_name": "Report Component", | ||||||
|  |                 "verbose_name_plural": "Report Components", | ||||||
|  |             }, | ||||||
|  |         ), | ||||||
|  |         migrations.CreateModel( | ||||||
|  |             name="Report", | ||||||
|  |             fields=[ | ||||||
|  |                 ( | ||||||
|  |                     "policybindingmodel_ptr", | ||||||
|  |                     models.OneToOneField( | ||||||
|  |                         auto_created=True, | ||||||
|  |                         on_delete=django.db.models.deletion.CASCADE, | ||||||
|  |                         parent_link=True, | ||||||
|  |                         primary_key=True, | ||||||
|  |                         serialize=False, | ||||||
|  |                         to="authentik_policies.policybindingmodel", | ||||||
|  |                     ), | ||||||
|  |                 ), | ||||||
|  |                 ("name", models.TextField()), | ||||||
|  |                 ("schedule", models.TextField()), | ||||||
|  |                 ("output_type", models.TextField(choices=[("csv", "Csv"), ("pdf", "Pdf")])), | ||||||
|  |                 ( | ||||||
|  |                     "delivery", | ||||||
|  |                     models.ForeignKey( | ||||||
|  |                         default=None, | ||||||
|  |                         null=True, | ||||||
|  |                         on_delete=django.db.models.deletion.SET_DEFAULT, | ||||||
|  |                         to="authentik_events.notificationtransport", | ||||||
|  |                     ), | ||||||
|  |                 ), | ||||||
|  |                 ( | ||||||
|  |                     "run_as", | ||||||
|  |                     models.ForeignKey( | ||||||
|  |                         default=None, | ||||||
|  |                         null=True, | ||||||
|  |                         on_delete=django.db.models.deletion.SET_DEFAULT, | ||||||
|  |                         to=settings.AUTH_USER_MODEL, | ||||||
|  |                     ), | ||||||
|  |                 ), | ||||||
|  |             ], | ||||||
|  |             options={ | ||||||
|  |                 "verbose_name": "Report", | ||||||
|  |                 "verbose_name_plural": "Reports", | ||||||
|  |             }, | ||||||
|  |             bases=("authentik_policies.policybindingmodel", models.Model), | ||||||
|  |         ), | ||||||
|  |         migrations.CreateModel( | ||||||
|  |             name="ReportComponentBinding", | ||||||
|  |             fields=[ | ||||||
|  |                 ( | ||||||
|  |                     "policybindingmodel_ptr", | ||||||
|  |                     models.OneToOneField( | ||||||
|  |                         auto_created=True, | ||||||
|  |                         on_delete=django.db.models.deletion.CASCADE, | ||||||
|  |                         parent_link=True, | ||||||
|  |                         to="authentik_policies.policybindingmodel", | ||||||
|  |                     ), | ||||||
|  |                 ), | ||||||
|  |                 ( | ||||||
|  |                     "binding_uuid", | ||||||
|  |                     models.UUIDField( | ||||||
|  |                         default=uuid.uuid4, editable=False, primary_key=True, serialize=False | ||||||
|  |                     ), | ||||||
|  |                 ), | ||||||
|  |                 ("enabled", models.BooleanField(default=True)), | ||||||
|  |                 ("layout_x", models.PositiveIntegerField(default=0)), | ||||||
|  |                 ("layout_y", models.PositiveIntegerField(default=0)), | ||||||
|  |                 ( | ||||||
|  |                     "target", | ||||||
|  |                     models.ForeignKey( | ||||||
|  |                         on_delete=django.db.models.deletion.CASCADE, to="authentik_reporting.report" | ||||||
|  |                     ), | ||||||
|  |                 ), | ||||||
|  |                 ( | ||||||
|  |                     "widget", | ||||||
|  |                     authentik.lib.models.InheritanceForeignKey( | ||||||
|  |                         on_delete=django.db.models.deletion.CASCADE, | ||||||
|  |                         related_name="+", | ||||||
|  |                         to="authentik_reporting.reportcomponent", | ||||||
|  |                     ), | ||||||
|  |                 ), | ||||||
|  |             ], | ||||||
|  |             options={ | ||||||
|  |                 "verbose_name": "Report Component Binding", | ||||||
|  |                 "verbose_name_plural": "Report Component Bindings", | ||||||
|  |                 "unique_together": {("target", "widget")}, | ||||||
|  |             }, | ||||||
|  |             bases=("authentik_policies.policybindingmodel", models.Model), | ||||||
|  |         ), | ||||||
|  |         migrations.AddField( | ||||||
|  |             model_name="report", | ||||||
|  |             name="components", | ||||||
|  |             field=models.ManyToManyField( | ||||||
|  |                 blank=True, | ||||||
|  |                 related_name="bindings", | ||||||
|  |                 through="authentik_reporting.ReportComponentBinding", | ||||||
|  |                 to="authentik_reporting.reportcomponent", | ||||||
|  |             ), | ||||||
|  |         ), | ||||||
|  |     ] | ||||||
							
								
								
									
										87
									
								
								authentik/enterprise/reporting/models.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								authentik/enterprise/reporting/models.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,87 @@ | |||||||
|  | """Reporting models""" | ||||||
|  |  | ||||||
|  | from uuid import uuid4 | ||||||
|  |  | ||||||
|  | from celery.schedules import crontab | ||||||
|  | from django.db import models | ||||||
|  | from django.utils.translation import gettext_lazy as _ | ||||||
|  |  | ||||||
|  | from authentik.events.models import NotificationTransport | ||||||
|  | from authentik.lib.models import InheritanceForeignKey, SerializerModel | ||||||
|  | from authentik.policies.models import PolicyBindingModel | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class OutputType(models.TextChoices): | ||||||
|  |     """Different choices in which a report can be 'rendered'""" | ||||||
|  |  | ||||||
|  |     csv = "csv" | ||||||
|  |     pdf = "pdf" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Report(SerializerModel, PolicyBindingModel): | ||||||
|  |     """A report with a defined list of components, which can run on a schedule""" | ||||||
|  |  | ||||||
|  |     name = models.TextField() | ||||||
|  |  | ||||||
|  |     schedule = models.TextField() | ||||||
|  |  | ||||||
|  |     # User under which permissions the queries are run, | ||||||
|  |     # when no user is selected the report is inactive | ||||||
|  |     run_as = models.ForeignKey( | ||||||
|  |         "authentik_core.user", on_delete=models.SET_DEFAULT, default=None, null=True | ||||||
|  |     ) | ||||||
|  |     components = models.ManyToManyField( | ||||||
|  |         "ReportComponent", through="ReportComponentBinding", related_name="bindings", blank=True | ||||||
|  |     ) | ||||||
|  |     output_type = models.TextField(choices=OutputType.choices) | ||||||
|  |     # Use notification transport to send report result (either link for webhook based? | ||||||
|  |     # maybe send full csv?) or fully rendered PDF via Email | ||||||
|  |     # when no transport is selected, reports are not sent anywhere but can be retrieved in authentik | ||||||
|  |     delivery = models.ForeignKey( | ||||||
|  |         NotificationTransport, on_delete=models.SET_DEFAULT, default=None, null=True | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     def __str__(self) -> str: | ||||||
|  |         return self.name | ||||||
|  |  | ||||||
|  |     def get_celery_schedule(self) -> crontab: | ||||||
|  |         return crontab(*self.schedule.split()) | ||||||
|  |  | ||||||
|  |     class Meta: | ||||||
|  |         verbose_name = _("Report") | ||||||
|  |         verbose_name_plural = _("Reports") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class ReportComponentBinding(SerializerModel, PolicyBindingModel): | ||||||
|  |     """Binding of a component to a report""" | ||||||
|  |  | ||||||
|  |     binding_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4) | ||||||
|  |  | ||||||
|  |     enabled = models.BooleanField(default=True) | ||||||
|  |  | ||||||
|  |     layout_x = models.PositiveIntegerField(default=0) | ||||||
|  |     layout_y = models.PositiveIntegerField(default=0) | ||||||
|  |  | ||||||
|  |     target = models.ForeignKey("Report", on_delete=models.CASCADE) | ||||||
|  |     widget = InheritanceForeignKey("ReportComponent", on_delete=models.CASCADE, related_name="+") | ||||||
|  |  | ||||||
|  |     def __str__(self) -> str: | ||||||
|  |         return f"Binding from {self.report.name} to {self.widget}" | ||||||
|  |  | ||||||
|  |     class Meta: | ||||||
|  |         verbose_name = _("Report Component Binding") | ||||||
|  |         verbose_name_plural = _("Report Component Bindings") | ||||||
|  |         unique_together = ("target", "widget") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class ReportComponent(SerializerModel): | ||||||
|  |     """An individual component of a report, a query or graph, etc""" | ||||||
|  |  | ||||||
|  |     widget_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4) | ||||||
|  |  | ||||||
|  |     def __str__(self) -> str: | ||||||
|  |         return super().__str__() | ||||||
|  |  | ||||||
|  |     class Meta: | ||||||
|  |         verbose_name = _("Report Component") | ||||||
|  |         verbose_name_plural = _("Report Components") | ||||||
							
								
								
									
										38
									
								
								authentik/enterprise/reporting/signals.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								authentik/enterprise/reporting/signals.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,38 @@ | |||||||
|  | from json import dumps | ||||||
|  |  | ||||||
|  | from django.db.models.signals import post_save, pre_delete | ||||||
|  | from django.dispatch import receiver | ||||||
|  | from django_celery_beat.models import CrontabSchedule, PeriodicTask | ||||||
|  |  | ||||||
|  | from authentik.enterprise.reporting.models import Report | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @receiver(post_save, sender=Report) | ||||||
|  | def report_post_save(sender, instance: Report, **_): | ||||||
|  |     if instance.schedule == "": | ||||||
|  |         return | ||||||
|  |     schedule = CrontabSchedule.from_schedule(instance.get_celery_schedule()) | ||||||
|  |     schedule.save() | ||||||
|  |     PeriodicTask.objects.update_or_create( | ||||||
|  |         name=str(instance.pk), | ||||||
|  |         defaults={ | ||||||
|  |             "crontab": schedule, | ||||||
|  |             "task": "authentik.enterprise.reporting.tasks.process_report", | ||||||
|  |             "queue": "authentik_reporting", | ||||||
|  |             "description": f"Report {instance.name}", | ||||||
|  |             "kwargs": dumps( | ||||||
|  |                 { | ||||||
|  |                     "report_uuid": str(instance.pk), | ||||||
|  |                 } | ||||||
|  |             ), | ||||||
|  |         }, | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @receiver(pre_delete, sender=Report) | ||||||
|  | def report_pre_delete(sender, instance: Report, **_): | ||||||
|  |     if instance.schedule == "": | ||||||
|  |         return | ||||||
|  |     PeriodicTask.objects.filter(name=str(instance.pk)).delete() | ||||||
|  |     # Cleanup schedules without any tasks | ||||||
|  |     CrontabSchedule.objects.filter(periodictask__isnull=True).delete() | ||||||
							
								
								
									
										11
									
								
								authentik/enterprise/reporting/tasks.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								authentik/enterprise/reporting/tasks.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,11 @@ | |||||||
|  | from authentik.enterprise.reporting.executor import ReportExecutor | ||||||
|  | from authentik.enterprise.reporting.models import Report | ||||||
|  | from authentik.root.celery import CELERY_APP | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @CELERY_APP.task() | ||||||
|  | def process_report(report_uuid: str): | ||||||
|  |     report = Report.objects.filter(pk=report_uuid).first() | ||||||
|  |     if not report or not report.run_as: | ||||||
|  |         return | ||||||
|  |     ReportExecutor(report).execute() | ||||||
| @ -17,6 +17,7 @@ TENANT_APPS = [ | |||||||
|     "authentik.enterprise.providers.google_workspace", |     "authentik.enterprise.providers.google_workspace", | ||||||
|     "authentik.enterprise.providers.microsoft_entra", |     "authentik.enterprise.providers.microsoft_entra", | ||||||
|     "authentik.enterprise.providers.ssf", |     "authentik.enterprise.providers.ssf", | ||||||
|  |     "authentik.enterprise.reporting", | ||||||
|     "authentik.enterprise.stages.authenticator_endpoint_gdtc", |     "authentik.enterprise.stages.authenticator_endpoint_gdtc", | ||||||
|     "authentik.enterprise.stages.source", |     "authentik.enterprise.stages.source", | ||||||
| ] | ] | ||||||
|  | |||||||
| @ -125,6 +125,7 @@ TENANT_APPS = [ | |||||||
|     "authentik.brands", |     "authentik.brands", | ||||||
|     "authentik.blueprints", |     "authentik.blueprints", | ||||||
|     "guardian", |     "guardian", | ||||||
|  |     "django_celery_beat", | ||||||
| ] | ] | ||||||
|  |  | ||||||
| TENANT_MODEL = "authentik_tenants.Tenant" | TENANT_MODEL = "authentik_tenants.Tenant" | ||||||
|  | |||||||
| @ -1,14 +1,18 @@ | |||||||
| """Tenant-aware Celery beat scheduler""" | """Tenant-aware Celery beat scheduler""" | ||||||
|  |  | ||||||
| from tenant_schemas_celery.scheduler import ( | from django_celery_beat.schedulers import DatabaseScheduler, ModelEntry | ||||||
|     TenantAwarePersistentScheduler as BaseTenantAwarePersistentScheduler, | from tenant_schemas_celery.scheduler import TenantAwareScheduleEntry, TenantAwareSchedulerMixin | ||||||
| ) |  | ||||||
| from tenant_schemas_celery.scheduler import TenantAwareScheduleEntry |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class TenantAwarePersistentScheduler(BaseTenantAwarePersistentScheduler): | class SchedulerEntry(ModelEntry, TenantAwareScheduleEntry): | ||||||
|  |     pass | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class TenantAwarePersistentScheduler(TenantAwareSchedulerMixin, DatabaseScheduler): | ||||||
|     """Tenant-aware Celery beat scheduler""" |     """Tenant-aware Celery beat scheduler""" | ||||||
|  |  | ||||||
|  |     Entry = SchedulerEntry | ||||||
|  |  | ||||||
|     @classmethod |     @classmethod | ||||||
|     def get_queryset(cls): |     def get_queryset(cls): | ||||||
|         return super().get_queryset().filter(ready=True) |         return super().get_queryset().filter(ready=True) | ||||||
|  | |||||||
							
								
								
									
										322
									
								
								poetry.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										322
									
								
								poetry.lock
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -92,6 +92,7 @@ dacite = "*" | |||||||
| deepmerge = "*" | deepmerge = "*" | ||||||
| defusedxml = "*" | defusedxml = "*" | ||||||
| django = "*" | django = "*" | ||||||
|  | django-celery-beat = "*" | ||||||
| django-countries = "*" | django-countries = "*" | ||||||
| django-cte = "*" | django-cte = "*" | ||||||
| django-filter = "*" | django-filter = "*" | ||||||
|  | |||||||
		Reference in New Issue
	
	Block a user
	