Compare commits
	
		
			2 Commits
		
	
	
		
			endpoints
			...
			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.microsoft_entra", | ||||
|     "authentik.enterprise.providers.ssf", | ||||
|     "authentik.enterprise.reporting", | ||||
|     "authentik.enterprise.stages.authenticator_endpoint_gdtc", | ||||
|     "authentik.enterprise.stages.source", | ||||
| ] | ||||
|  | ||||
| @ -125,6 +125,7 @@ TENANT_APPS = [ | ||||
|     "authentik.brands", | ||||
|     "authentik.blueprints", | ||||
|     "guardian", | ||||
|     "django_celery_beat", | ||||
| ] | ||||
|  | ||||
| TENANT_MODEL = "authentik_tenants.Tenant" | ||||
|  | ||||
| @ -1,14 +1,18 @@ | ||||
| """Tenant-aware Celery beat scheduler""" | ||||
|  | ||||
| from tenant_schemas_celery.scheduler import ( | ||||
|     TenantAwarePersistentScheduler as BaseTenantAwarePersistentScheduler, | ||||
| ) | ||||
| from tenant_schemas_celery.scheduler import TenantAwareScheduleEntry | ||||
| from django_celery_beat.schedulers import DatabaseScheduler, ModelEntry | ||||
| from tenant_schemas_celery.scheduler import TenantAwareScheduleEntry, TenantAwareSchedulerMixin | ||||
|  | ||||
|  | ||||
| class TenantAwarePersistentScheduler(BaseTenantAwarePersistentScheduler): | ||||
| class SchedulerEntry(ModelEntry, TenantAwareScheduleEntry): | ||||
|     pass | ||||
|  | ||||
|  | ||||
| class TenantAwarePersistentScheduler(TenantAwareSchedulerMixin, DatabaseScheduler): | ||||
|     """Tenant-aware Celery beat scheduler""" | ||||
|  | ||||
|     Entry = SchedulerEntry | ||||
|  | ||||
|     @classmethod | ||||
|     def get_queryset(cls): | ||||
|         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 = "*" | ||||
| defusedxml = "*" | ||||
| django = "*" | ||||
| django-celery-beat = "*" | ||||
| django-countries = "*" | ||||
| django-cte = "*" | ||||
| django-filter = "*" | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	