Compare commits
	
		
			3 Commits
		
	
	
		
			website/do
			...
			analytics
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 435ba598bb | |||
| 582511abcc | |||
| 80ea1dae81 | 
							
								
								
									
										20
									
								
								authentik/admin/analytics.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								authentik/admin/analytics.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,20 @@ | |||||||
|  | """authentik admin analytics""" | ||||||
|  |  | ||||||
|  | from typing import Any | ||||||
|  |  | ||||||
|  | from django.utils.translation import gettext_lazy as _ | ||||||
|  |  | ||||||
|  | from authentik.root.celery import CELERY_APP | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def get_analytics_description() -> dict[str, str]: | ||||||
|  |     return { | ||||||
|  |         "worker_count": _("Number of running workers"), | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def get_analytics_data() -> dict[str, Any]: | ||||||
|  |     worker_count = len(CELERY_APP.control.ping(timeout=0.5)) | ||||||
|  |     return { | ||||||
|  |         "worker_count": worker_count, | ||||||
|  |     } | ||||||
							
								
								
									
										0
									
								
								authentik/analytics/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								authentik/analytics/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										54
									
								
								authentik/analytics/api.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								authentik/analytics/api.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,54 @@ | |||||||
|  | """authentik analytics api""" | ||||||
|  |  | ||||||
|  | from drf_spectacular.utils import extend_schema, inline_serializer | ||||||
|  | from rest_framework.fields import CharField, DictField | ||||||
|  | from rest_framework.request import Request | ||||||
|  | from rest_framework.response import Response | ||||||
|  | from rest_framework.viewsets import ViewSet | ||||||
|  |  | ||||||
|  | from authentik.analytics.utils import get_analytics_data, get_analytics_description | ||||||
|  | from authentik.core.api.utils import PassiveSerializer | ||||||
|  | from authentik.rbac.permissions import HasPermission | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class AnalyticsDescriptionSerializer(PassiveSerializer): | ||||||
|  |     label = CharField() | ||||||
|  |     desc = CharField() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class AnalyticsDescriptionViewSet(ViewSet): | ||||||
|  |     """Read-only view of analytics descriptions""" | ||||||
|  |  | ||||||
|  |     permission_classes = [HasPermission("authentik_rbac.view_system_settings")] | ||||||
|  |  | ||||||
|  |     @extend_schema(responses={200: AnalyticsDescriptionSerializer}) | ||||||
|  |     def list(self, request: Request) -> Response: | ||||||
|  |         """Read-only view of analytics descriptions""" | ||||||
|  |         data = [] | ||||||
|  |         for label, desc in get_analytics_description().items(): | ||||||
|  |             data.append({"label": label, "desc": desc}) | ||||||
|  |         return Response(AnalyticsDescriptionSerializer(data, many=True).data) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class AnalyticsDataViewSet(ViewSet): | ||||||
|  |     """Read-only view of analytics descriptions""" | ||||||
|  |  | ||||||
|  |     permission_classes = [HasPermission("authentik_rbac.edit_system_settings")] | ||||||
|  |  | ||||||
|  |     @extend_schema( | ||||||
|  |         responses={ | ||||||
|  |             200: inline_serializer( | ||||||
|  |                 name="AnalyticsData", | ||||||
|  |                 fields={ | ||||||
|  |                     "data": DictField(), | ||||||
|  |                 }, | ||||||
|  |             ) | ||||||
|  |         } | ||||||
|  |     ) | ||||||
|  |     def list(self, request: Request) -> Response: | ||||||
|  |         """Read-only view of analytics descriptions""" | ||||||
|  |         return Response( | ||||||
|  |             { | ||||||
|  |                 "data": get_analytics_data(force=True), | ||||||
|  |             } | ||||||
|  |         ) | ||||||
							
								
								
									
										12
									
								
								authentik/analytics/apps.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								authentik/analytics/apps.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,12 @@ | |||||||
|  | """authentik analytics app config""" | ||||||
|  |  | ||||||
|  | from authentik.blueprints.apps import ManagedAppConfig | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class AuthentikAdminConfig(ManagedAppConfig): | ||||||
|  |     """authentik analytics app config""" | ||||||
|  |  | ||||||
|  |     name = "authentik.analytics" | ||||||
|  |     label = "authentik_analytics" | ||||||
|  |     verbose_name = "authentik Analytics" | ||||||
|  |     default = True | ||||||
							
								
								
									
										19
									
								
								authentik/analytics/models.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								authentik/analytics/models.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,19 @@ | |||||||
|  | """authentik analytics mixins""" | ||||||
|  |  | ||||||
|  | from typing import Any | ||||||
|  |  | ||||||
|  | from django.utils.translation import gettext_lazy as _ | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class AnalyticsMixin: | ||||||
|  |     @classmethod | ||||||
|  |     def get_analytics_description(cls) -> dict[str, str]: | ||||||
|  |         object_name = _(cls._meta.verbose_name) | ||||||
|  |         count_desc = _("Number of {object_name} objects".format_map({"object_name": object_name})) | ||||||
|  |         return { | ||||||
|  |             "count": count_desc, | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def get_analytics_data(cls) -> dict[str, Any]: | ||||||
|  |         return {"count": cls.objects.all().count()} | ||||||
							
								
								
									
										17
									
								
								authentik/analytics/settings.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								authentik/analytics/settings.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,17 @@ | |||||||
|  | """authentik admin settings""" | ||||||
|  |  | ||||||
|  | from celery.schedules import crontab | ||||||
|  |  | ||||||
|  | from authentik.lib.utils.time import fqdn_rand | ||||||
|  |  | ||||||
|  | CELERY_BEAT_SCHEDULE = { | ||||||
|  |     "analytics_send": { | ||||||
|  |         "task": "authentik.analytics.tasks.send_analytics", | ||||||
|  |         "schedule": crontab( | ||||||
|  |             minute=fqdn_rand("analytics_send"), | ||||||
|  |             hour=fqdn_rand("analytics_send", stop=24), | ||||||
|  |             day_of_week=fqdn_rand("analytics_send", 7), | ||||||
|  |         ), | ||||||
|  |         "options": {"queue": "authentik_scheduled"}, | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										45
									
								
								authentik/analytics/tasks.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								authentik/analytics/tasks.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,45 @@ | |||||||
|  | """authentik admin tasks""" | ||||||
|  |  | ||||||
|  | import orjson | ||||||
|  | from django.utils.translation import gettext_lazy as _ | ||||||
|  | from requests import RequestException | ||||||
|  | from structlog.stdlib import get_logger | ||||||
|  |  | ||||||
|  | from authentik.analytics.utils import get_analytics_data | ||||||
|  | from authentik.events.models import Event, EventAction | ||||||
|  | from authentik.events.system_tasks import SystemTask, TaskStatus, prefill_task | ||||||
|  | from authentik.lib.utils.http import get_http_session | ||||||
|  | from authentik.root.celery import CELERY_APP | ||||||
|  | from authentik.tenants.models import Tenant | ||||||
|  |  | ||||||
|  | LOGGER = get_logger() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @CELERY_APP.task(bind=True, base=SystemTask) | ||||||
|  | @prefill_task | ||||||
|  | def send_analytics(self: SystemTask): | ||||||
|  |     """Send analytics""" | ||||||
|  |     for tenant in Tenant.objects.filter(ready=True): | ||||||
|  |         data = get_analytics_data(current_tenant=tenant) | ||||||
|  |         if not tenant.analytics_enabled or not data: | ||||||
|  |             self.set_status(TaskStatus.WARNING, "Analytics disabled. Nothing was sent.") | ||||||
|  |             return | ||||||
|  |         try: | ||||||
|  |             response = get_http_session().post( | ||||||
|  |                 "https://customers.goauthentik.io/api/analytics/post/", json=data | ||||||
|  |             ) | ||||||
|  |             response.raise_for_status() | ||||||
|  |             self.set_status( | ||||||
|  |                 TaskStatus.SUCCESSFUL, | ||||||
|  |                 "Successfully sent analytics", | ||||||
|  |                 orjson.dumps( | ||||||
|  |                     data, option=orjson.OPT_INDENT_2 | orjson.OPT_NON_STR_KEYS | orjson.OPT_UTC_Z | ||||||
|  |                 ).decode(), | ||||||
|  |             ) | ||||||
|  |             Event.new( | ||||||
|  |                 EventAction.ANALYTICS_SENT, | ||||||
|  |                 message=_("Analytics sent"), | ||||||
|  |                 analytics_data=data, | ||||||
|  |             ).save() | ||||||
|  |         except (RequestException, IndexError) as exc: | ||||||
|  |             self.set_error(exc) | ||||||
							
								
								
									
										76
									
								
								authentik/analytics/tests.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								authentik/analytics/tests.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,76 @@ | |||||||
|  | """authentik analytics tests""" | ||||||
|  |  | ||||||
|  | from json import loads | ||||||
|  | from requests_mock import Mocker | ||||||
|  |  | ||||||
|  | from django.test import TestCase | ||||||
|  | from django.urls import reverse | ||||||
|  |  | ||||||
|  | from authentik import __version__ | ||||||
|  | from authentik.analytics.tasks import send_analytics | ||||||
|  | from authentik.analytics.utils import get_analytics_apps_data, get_analytics_apps_description, get_analytics_data, get_analytics_description, get_analytics_models_data, get_analytics_models_description | ||||||
|  | from authentik.core.models import Group, User | ||||||
|  | from authentik.events.models import Event, EventAction | ||||||
|  | from authentik.lib.generators import generate_id | ||||||
|  | from authentik.tenants.utils import get_current_tenant | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class TestAnalytics(TestCase): | ||||||
|  |     """test analytics api""" | ||||||
|  |  | ||||||
|  |     def setUp(self) -> None: | ||||||
|  |         super().setUp() | ||||||
|  |         self.user = User.objects.create(username=generate_id()) | ||||||
|  |         self.group = Group.objects.create(name=generate_id(), is_superuser=True) | ||||||
|  |         self.group.users.add(self.user) | ||||||
|  |         self.client.force_login(self.user) | ||||||
|  |         self.tenant = get_current_tenant() | ||||||
|  |  | ||||||
|  |     def test_description_api(self): | ||||||
|  |         """Test Version API""" | ||||||
|  |         response = self.client.get(reverse("authentik_api:analytics-description-list")) | ||||||
|  |         self.assertEqual(response.status_code, 200) | ||||||
|  |         loads(response.content) | ||||||
|  |  | ||||||
|  |     def test_data_api(self): | ||||||
|  |         """Test Version API""" | ||||||
|  |         response = self.client.get(reverse("authentik_api:analytics-data-list")) | ||||||
|  |         self.assertEqual(response.status_code, 200) | ||||||
|  |         body = loads(response.content) | ||||||
|  |         self.assertEqual(body["data"]["version"], __version__) | ||||||
|  |  | ||||||
|  |     def test_sending_enabled(self): | ||||||
|  |         """Test analytics sending""" | ||||||
|  |         self.tenant.analytics_enabled = True | ||||||
|  |         self.tenant.save() | ||||||
|  |         with Mocker() as mocker: | ||||||
|  |             mocker.post("https://customers.goauthentik.io/api/analytics/post/", status_code=200) | ||||||
|  |             send_analytics.delay().get() | ||||||
|  |             self.assertTrue( | ||||||
|  |                 Event.objects.filter( | ||||||
|  |                     action=EventAction.ANALYTICS_SENT | ||||||
|  |                 ).exists() | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |     def test_sending_disabled(self): | ||||||
|  |         """Test analytics sending""" | ||||||
|  |         self.tenant.analytics_enabled = False | ||||||
|  |         self.tenant.save() | ||||||
|  |         send_analytics.delay().get() | ||||||
|  |         self.assertFalse( | ||||||
|  |             Event.objects.filter( | ||||||
|  |                 action=EventAction.ANALYTICS_SENT | ||||||
|  |             ).exists() | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |     def test_description_data_match_apps(self): | ||||||
|  |         """Test description and data keys match""" | ||||||
|  |         description = get_analytics_apps_description() | ||||||
|  |         data = get_analytics_apps_data() | ||||||
|  |         self.assertEqual(data.keys(), description.keys()) | ||||||
|  |  | ||||||
|  |     def test_description_data_match_models(self): | ||||||
|  |         """Test description and data keys match""" | ||||||
|  |         description = get_analytics_models_description() | ||||||
|  |         data = get_analytics_models_data() | ||||||
|  |         self.assertEqual(data.keys(), description.keys()) | ||||||
							
								
								
									
										8
									
								
								authentik/analytics/urls.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								authentik/analytics/urls.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,8 @@ | |||||||
|  | """API URLs""" | ||||||
|  |  | ||||||
|  | from authentik.analytics.api import AnalyticsDataViewSet, AnalyticsDescriptionViewSet | ||||||
|  |  | ||||||
|  | api_urlpatterns = [ | ||||||
|  |     ("analytics/description", AnalyticsDescriptionViewSet, "analytics-description"), | ||||||
|  |     ("analytics/data", AnalyticsDataViewSet, "analytics-data"), | ||||||
|  | ] | ||||||
							
								
								
									
										112
									
								
								authentik/analytics/utils.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								authentik/analytics/utils.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,112 @@ | |||||||
|  | """authentik analytics utils""" | ||||||
|  |  | ||||||
|  | from hashlib import sha256 | ||||||
|  | from importlib import import_module | ||||||
|  | from typing import Any | ||||||
|  |  | ||||||
|  | from structlog import get_logger | ||||||
|  |  | ||||||
|  | from authentik import get_full_version | ||||||
|  | from authentik.analytics.models import AnalyticsMixin | ||||||
|  | from authentik.lib.utils.reflection import get_apps | ||||||
|  | from authentik.root.install_id import get_install_id | ||||||
|  | from authentik.tenants.models import Tenant | ||||||
|  | from authentik.tenants.utils import get_current_tenant | ||||||
|  |  | ||||||
|  | LOGGER = get_logger() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def get_analytics_apps() -> dict: | ||||||
|  |     modules = {} | ||||||
|  |     for _authentik_app in get_apps(): | ||||||
|  |         try: | ||||||
|  |             module = import_module(f"{_authentik_app.name}.analytics") | ||||||
|  |         except ModuleNotFoundError: | ||||||
|  |             continue | ||||||
|  |         except ImportError as exc: | ||||||
|  |             LOGGER.warning( | ||||||
|  |                 "Could not import app's analytics", app_name=_authentik_app.name, exc=exc | ||||||
|  |             ) | ||||||
|  |             continue | ||||||
|  |         if not hasattr(module, "get_analytics_description") or not hasattr( | ||||||
|  |             module, "get_analytics_data" | ||||||
|  |         ): | ||||||
|  |             LOGGER.debug( | ||||||
|  |                 "App does not define API URLs", | ||||||
|  |                 app_name=_authentik_app.name, | ||||||
|  |             ) | ||||||
|  |             continue | ||||||
|  |         modules[_authentik_app.label] = module | ||||||
|  |     return modules | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def get_analytics_apps_description() -> dict[str, str]: | ||||||
|  |     result = {} | ||||||
|  |     for app_label, module in get_analytics_apps().items(): | ||||||
|  |         for k, v in module.get_analytics_description().items(): | ||||||
|  |             result[f"{app_label}/app/{k}"] = v | ||||||
|  |     return result | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def get_analytics_apps_data() -> dict[str, Any]: | ||||||
|  |     result = {} | ||||||
|  |     for app_label, module in get_analytics_apps().items(): | ||||||
|  |         for k, v in module.get_analytics_data().items(): | ||||||
|  |             result[f"{app_label}/app/{k}"] = v | ||||||
|  |     return result | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def get_analytics_models() -> list[AnalyticsMixin]: | ||||||
|  |     def get_subclasses(cls): | ||||||
|  |         for subclass in cls.__subclasses__(): | ||||||
|  |             if subclass.__subclasses__(): | ||||||
|  |                 yield from get_subclasses(subclass) | ||||||
|  |             elif not subclass._meta.abstract: | ||||||
|  |                 yield subclass | ||||||
|  |  | ||||||
|  |     return list(get_subclasses(AnalyticsMixin)) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def get_analytics_models_description() -> dict[str, str]: | ||||||
|  |     result = {} | ||||||
|  |     for model in get_analytics_models(): | ||||||
|  |         for k, v in model.get_analytics_description().items(): | ||||||
|  |             result[f"{model._meta.app_label}/models/{model._meta.object_name}/{k}"] = v | ||||||
|  |     return result | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def get_analytics_models_data() -> dict[str, Any]: | ||||||
|  |     result = {} | ||||||
|  |     for model in get_analytics_models(): | ||||||
|  |         for k, v in model.get_analytics_data().items(): | ||||||
|  |             result[f"{model._meta.app_label}/models/{model._meta.object_name}/{k}"] = v | ||||||
|  |     return result | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def get_analytics_description() -> dict[str, str]: | ||||||
|  |     return { | ||||||
|  |         **get_analytics_apps_description(), | ||||||
|  |         **get_analytics_models_description(), | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def get_analytics_data(current_tenant: Tenant | None = None, force: bool = False) -> dict[str, Any]: | ||||||
|  |     current_tenant = current_tenant or get_current_tenant() | ||||||
|  |     if not current_tenant.analytics_enabled and not force: | ||||||
|  |         return {} | ||||||
|  |     data = { | ||||||
|  |         **get_analytics_apps_data(), | ||||||
|  |         **get_analytics_models_data(), | ||||||
|  |     } | ||||||
|  |     to_remove = [] | ||||||
|  |     for key in data.keys(): | ||||||
|  |         if key not in current_tenant.analytics_sources: | ||||||
|  |             to_remove.append(key) | ||||||
|  |     for key in to_remove: | ||||||
|  |         del data[key] | ||||||
|  |     return { | ||||||
|  |         **data, | ||||||
|  |         "install_id_hash": sha256(get_install_id().encode()).hexdigest(), | ||||||
|  |         "tenant_hash": sha256(current_tenant.tenant_uuid.bytes).hexdigest(), | ||||||
|  |         "version": get_full_version(), | ||||||
|  |     } | ||||||
| @ -23,6 +23,7 @@ from model_utils.managers import InheritanceManager | |||||||
| from rest_framework.serializers import Serializer | from rest_framework.serializers import Serializer | ||||||
| from structlog.stdlib import get_logger | from structlog.stdlib import get_logger | ||||||
|  |  | ||||||
|  | from authentik.analytics.models import AnalyticsMixin | ||||||
| from authentik.blueprints.models import ManagedModel | from authentik.blueprints.models import ManagedModel | ||||||
| from authentik.core.expression.exceptions import PropertyMappingExpressionException | from authentik.core.expression.exceptions import PropertyMappingExpressionException | ||||||
| from authentik.core.types import UILoginButton, UserSettingSerializer | from authentik.core.types import UILoginButton, UserSettingSerializer | ||||||
| @ -168,7 +169,7 @@ class GroupQuerySet(CTEQuerySet): | |||||||
|         return cte.join(Group, group_uuid=cte.col.group_uuid).with_cte(cte) |         return cte.join(Group, group_uuid=cte.col.group_uuid).with_cte(cte) | ||||||
|  |  | ||||||
|  |  | ||||||
| class Group(SerializerModel, AttributesMixin): | class Group(SerializerModel, AttributesMixin, AnalyticsMixin): | ||||||
|     """Group model which supports a basic hierarchy and has attributes""" |     """Group model which supports a basic hierarchy and has attributes""" | ||||||
|  |  | ||||||
|     group_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4) |     group_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4) | ||||||
| @ -258,7 +259,7 @@ class UserManager(DjangoUserManager): | |||||||
|         return self.get_queryset().exclude_anonymous() |         return self.get_queryset().exclude_anonymous() | ||||||
|  |  | ||||||
|  |  | ||||||
| class User(SerializerModel, GuardianUserMixin, AttributesMixin, AbstractUser): | class User(SerializerModel, GuardianUserMixin, AttributesMixin, AbstractUser, AnalyticsMixin): | ||||||
|     """authentik User model, based on django's contrib auth user model.""" |     """authentik User model, based on django's contrib auth user model.""" | ||||||
|  |  | ||||||
|     uuid = models.UUIDField(default=uuid4, editable=False, unique=True) |     uuid = models.UUIDField(default=uuid4, editable=False, unique=True) | ||||||
| @ -376,7 +377,7 @@ class User(SerializerModel, GuardianUserMixin, AttributesMixin, AbstractUser): | |||||||
|         return get_avatar(self) |         return get_avatar(self) | ||||||
|  |  | ||||||
|  |  | ||||||
| class Provider(SerializerModel): | class Provider(SerializerModel, AnalyticsMixin): | ||||||
|     """Application-independent Provider instance. For example SAML2 Remote, OAuth2 Application""" |     """Application-independent Provider instance. For example SAML2 Remote, OAuth2 Application""" | ||||||
|  |  | ||||||
|     name = models.TextField(unique=True) |     name = models.TextField(unique=True) | ||||||
| @ -470,7 +471,7 @@ class ApplicationQuerySet(QuerySet): | |||||||
|         return qs |         return qs | ||||||
|  |  | ||||||
|  |  | ||||||
| class Application(SerializerModel, PolicyBindingModel): | class Application(SerializerModel, PolicyBindingModel, AnalyticsMixin): | ||||||
|     """Every Application which uses authentik for authentication/identification/authorization |     """Every Application which uses authentik for authentication/identification/authorization | ||||||
|     needs an Application record. Other authentication types can subclass this Model to |     needs an Application record. Other authentication types can subclass this Model to | ||||||
|     add custom fields and other properties""" |     add custom fields and other properties""" | ||||||
| @ -603,7 +604,7 @@ class SourceGroupMatchingModes(models.TextChoices): | |||||||
|     ) |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
| class Source(ManagedModel, SerializerModel, PolicyBindingModel): | class Source(ManagedModel, SerializerModel, PolicyBindingModel, AnalyticsMixin): | ||||||
|     """Base Authentication source, i.e. an OAuth Provider, SAML Remote or LDAP Server""" |     """Base Authentication source, i.e. an OAuth Provider, SAML Remote or LDAP Server""" | ||||||
|  |  | ||||||
|     name = models.TextField(help_text=_("Source's display Name.")) |     name = models.TextField(help_text=_("Source's display Name.")) | ||||||
| @ -735,7 +736,7 @@ class Source(ManagedModel, SerializerModel, PolicyBindingModel): | |||||||
|         ] |         ] | ||||||
|  |  | ||||||
|  |  | ||||||
| class UserSourceConnection(SerializerModel, CreatedUpdatedModel): | class UserSourceConnection(SerializerModel, CreatedUpdatedModel, AnalyticsMixin): | ||||||
|     """Connection between User and Source.""" |     """Connection between User and Source.""" | ||||||
|  |  | ||||||
|     user = models.ForeignKey(User, on_delete=models.CASCADE) |     user = models.ForeignKey(User, on_delete=models.CASCADE) | ||||||
| @ -755,7 +756,7 @@ class UserSourceConnection(SerializerModel, CreatedUpdatedModel): | |||||||
|         unique_together = (("user", "source"),) |         unique_together = (("user", "source"),) | ||||||
|  |  | ||||||
|  |  | ||||||
| class GroupSourceConnection(SerializerModel, CreatedUpdatedModel): | class GroupSourceConnection(SerializerModel, CreatedUpdatedModel, AnalyticsMixin): | ||||||
|     """Connection between Group and Source.""" |     """Connection between Group and Source.""" | ||||||
|  |  | ||||||
|     group = models.ForeignKey(Group, on_delete=models.CASCADE) |     group = models.ForeignKey(Group, on_delete=models.CASCADE) | ||||||
| @ -879,7 +880,7 @@ class Token(SerializerModel, ManagedModel, ExpiringModel): | |||||||
|         ).save() |         ).save() | ||||||
|  |  | ||||||
|  |  | ||||||
| class PropertyMapping(SerializerModel, ManagedModel): | class PropertyMapping(SerializerModel, ManagedModel, AnalyticsMixin): | ||||||
|     """User-defined key -> x mapping which can be used by providers to expose extra data.""" |     """User-defined key -> x mapping which can be used by providers to expose extra data.""" | ||||||
|  |  | ||||||
|     pm_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4) |     pm_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4) | ||||||
|  | |||||||
							
								
								
									
										49
									
								
								authentik/events/migrations/0008_alter_event_action.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								authentik/events/migrations/0008_alter_event_action.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,49 @@ | |||||||
|  | # Generated by Django 5.0.9 on 2024-09-25 11:06 | ||||||
|  |  | ||||||
|  | from django.db import migrations, models | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Migration(migrations.Migration): | ||||||
|  |  | ||||||
|  |     dependencies = [ | ||||||
|  |         ("authentik_events", "0007_event_authentik_e_action_9a9dd9_idx_and_more"), | ||||||
|  |     ] | ||||||
|  |  | ||||||
|  |     operations = [ | ||||||
|  |         migrations.AlterField( | ||||||
|  |             model_name="event", | ||||||
|  |             name="action", | ||||||
|  |             field=models.TextField( | ||||||
|  |                 choices=[ | ||||||
|  |                     ("login", "Login"), | ||||||
|  |                     ("login_failed", "Login Failed"), | ||||||
|  |                     ("logout", "Logout"), | ||||||
|  |                     ("user_write", "User Write"), | ||||||
|  |                     ("suspicious_request", "Suspicious Request"), | ||||||
|  |                     ("password_set", "Password Set"), | ||||||
|  |                     ("secret_view", "Secret View"), | ||||||
|  |                     ("secret_rotate", "Secret Rotate"), | ||||||
|  |                     ("invitation_used", "Invite Used"), | ||||||
|  |                     ("authorize_application", "Authorize Application"), | ||||||
|  |                     ("source_linked", "Source Linked"), | ||||||
|  |                     ("impersonation_started", "Impersonation Started"), | ||||||
|  |                     ("impersonation_ended", "Impersonation Ended"), | ||||||
|  |                     ("flow_execution", "Flow Execution"), | ||||||
|  |                     ("policy_execution", "Policy Execution"), | ||||||
|  |                     ("policy_exception", "Policy Exception"), | ||||||
|  |                     ("property_mapping_exception", "Property Mapping Exception"), | ||||||
|  |                     ("system_task_execution", "System Task Execution"), | ||||||
|  |                     ("system_task_exception", "System Task Exception"), | ||||||
|  |                     ("system_exception", "System Exception"), | ||||||
|  |                     ("configuration_error", "Configuration Error"), | ||||||
|  |                     ("model_created", "Model Created"), | ||||||
|  |                     ("model_updated", "Model Updated"), | ||||||
|  |                     ("model_deleted", "Model Deleted"), | ||||||
|  |                     ("email_sent", "Email Sent"), | ||||||
|  |                     ("analytics_sent", "Analytics Sent"), | ||||||
|  |                     ("update_available", "Update Available"), | ||||||
|  |                     ("custom_", "Custom Prefix"), | ||||||
|  |                 ] | ||||||
|  |             ), | ||||||
|  |         ), | ||||||
|  |     ] | ||||||
| @ -119,6 +119,7 @@ class EventAction(models.TextChoices): | |||||||
|     MODEL_DELETED = "model_deleted" |     MODEL_DELETED = "model_deleted" | ||||||
|     EMAIL_SENT = "email_sent" |     EMAIL_SENT = "email_sent" | ||||||
|  |  | ||||||
|  |     ANALYTICS_SENT = "analytics_sent" | ||||||
|     UPDATE_AVAILABLE = "update_available" |     UPDATE_AVAILABLE = "update_available" | ||||||
|  |  | ||||||
|     CUSTOM_PREFIX = "custom_" |     CUSTOM_PREFIX = "custom_" | ||||||
|  | |||||||
| @ -0,0 +1,52 @@ | |||||||
|  | # Generated by Django 5.0.9 on 2024-09-25 11:06 | ||||||
|  |  | ||||||
|  | from django.db import migrations, models | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Migration(migrations.Migration): | ||||||
|  |  | ||||||
|  |     dependencies = [ | ||||||
|  |         ("authentik_policies_event_matcher", "0023_alter_eventmatcherpolicy_action_and_more"), | ||||||
|  |     ] | ||||||
|  |  | ||||||
|  |     operations = [ | ||||||
|  |         migrations.AlterField( | ||||||
|  |             model_name="eventmatcherpolicy", | ||||||
|  |             name="action", | ||||||
|  |             field=models.TextField( | ||||||
|  |                 choices=[ | ||||||
|  |                     ("login", "Login"), | ||||||
|  |                     ("login_failed", "Login Failed"), | ||||||
|  |                     ("logout", "Logout"), | ||||||
|  |                     ("user_write", "User Write"), | ||||||
|  |                     ("suspicious_request", "Suspicious Request"), | ||||||
|  |                     ("password_set", "Password Set"), | ||||||
|  |                     ("secret_view", "Secret View"), | ||||||
|  |                     ("secret_rotate", "Secret Rotate"), | ||||||
|  |                     ("invitation_used", "Invite Used"), | ||||||
|  |                     ("authorize_application", "Authorize Application"), | ||||||
|  |                     ("source_linked", "Source Linked"), | ||||||
|  |                     ("impersonation_started", "Impersonation Started"), | ||||||
|  |                     ("impersonation_ended", "Impersonation Ended"), | ||||||
|  |                     ("flow_execution", "Flow Execution"), | ||||||
|  |                     ("policy_execution", "Policy Execution"), | ||||||
|  |                     ("policy_exception", "Policy Exception"), | ||||||
|  |                     ("property_mapping_exception", "Property Mapping Exception"), | ||||||
|  |                     ("system_task_execution", "System Task Execution"), | ||||||
|  |                     ("system_task_exception", "System Task Exception"), | ||||||
|  |                     ("system_exception", "System Exception"), | ||||||
|  |                     ("configuration_error", "Configuration Error"), | ||||||
|  |                     ("model_created", "Model Created"), | ||||||
|  |                     ("model_updated", "Model Updated"), | ||||||
|  |                     ("model_deleted", "Model Deleted"), | ||||||
|  |                     ("email_sent", "Email Sent"), | ||||||
|  |                     ("analytics_sent", "Analytics Sent"), | ||||||
|  |                     ("update_available", "Update Available"), | ||||||
|  |                     ("custom_", "Custom Prefix"), | ||||||
|  |                 ], | ||||||
|  |                 default=None, | ||||||
|  |                 help_text="Match created events with this action type. When left empty, all action types will be matched.", | ||||||
|  |                 null=True, | ||||||
|  |             ), | ||||||
|  |         ), | ||||||
|  |     ] | ||||||
| @ -70,6 +70,7 @@ TENANT_APPS = [ | |||||||
|     "django.contrib.contenttypes", |     "django.contrib.contenttypes", | ||||||
|     "django.contrib.sessions", |     "django.contrib.sessions", | ||||||
|     "authentik.admin", |     "authentik.admin", | ||||||
|  |     "authentik.analytics", | ||||||
|     "authentik.api", |     "authentik.api", | ||||||
|     "authentik.crypto", |     "authentik.crypto", | ||||||
|     "authentik.flows", |     "authentik.flows", | ||||||
|  | |||||||
| @ -1,9 +1,12 @@ | |||||||
| """Serializer for tenants models""" | """Serializer for tenants models""" | ||||||
|  |  | ||||||
| from django_tenants.utils import get_public_schema_name | from django_tenants.utils import get_public_schema_name | ||||||
|  | from rest_framework.fields import SerializerMethodField | ||||||
| from rest_framework.generics import RetrieveUpdateAPIView | from rest_framework.generics import RetrieveUpdateAPIView | ||||||
| from rest_framework.permissions import SAFE_METHODS | from rest_framework.permissions import SAFE_METHODS | ||||||
|  |  | ||||||
|  | from authentik.analytics.api import AnalyticsDescriptionSerializer | ||||||
|  | from authentik.analytics.utils import get_analytics_description | ||||||
| from authentik.core.api.utils import ModelSerializer | from authentik.core.api.utils import ModelSerializer | ||||||
| from authentik.rbac.permissions import HasPermission | from authentik.rbac.permissions import HasPermission | ||||||
| from authentik.tenants.models import Tenant | from authentik.tenants.models import Tenant | ||||||
| @ -12,6 +15,8 @@ from authentik.tenants.models import Tenant | |||||||
| class SettingsSerializer(ModelSerializer): | class SettingsSerializer(ModelSerializer): | ||||||
|     """Settings Serializer""" |     """Settings Serializer""" | ||||||
|  |  | ||||||
|  |     analytics_sources_obj = SerializerMethodField() | ||||||
|  |  | ||||||
|     class Meta: |     class Meta: | ||||||
|         model = Tenant |         model = Tenant | ||||||
|         fields = [ |         fields = [ | ||||||
| @ -25,8 +30,19 @@ class SettingsSerializer(ModelSerializer): | |||||||
|             "impersonation", |             "impersonation", | ||||||
|             "default_token_duration", |             "default_token_duration", | ||||||
|             "default_token_length", |             "default_token_length", | ||||||
|  |             "default_token_length", | ||||||
|  |             "analytics_enabled", | ||||||
|  |             "analytics_sources", | ||||||
|  |             "analytics_sources_obj", | ||||||
|         ] |         ] | ||||||
|  |  | ||||||
|  |     def get_analytics_sources_obj(self, obj: Tenant) -> list[AnalyticsDescriptionSerializer]: | ||||||
|  |         result = [] | ||||||
|  |         for label, desc in get_analytics_description().items(): | ||||||
|  |             if label in obj.analytics_sources: | ||||||
|  |                 result.append((label, desc)) | ||||||
|  |         return result | ||||||
|  |  | ||||||
|  |  | ||||||
| class SettingsView(RetrieveUpdateAPIView): | class SettingsView(RetrieveUpdateAPIView): | ||||||
|     """Settings view""" |     """Settings view""" | ||||||
|  | |||||||
| @ -0,0 +1,26 @@ | |||||||
|  | # Generated by Django 5.0.9 on 2024-09-24 15:36 | ||||||
|  |  | ||||||
|  | import django.contrib.postgres.fields | ||||||
|  | from django.db import migrations, models | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Migration(migrations.Migration): | ||||||
|  |  | ||||||
|  |     dependencies = [ | ||||||
|  |         ("authentik_tenants", "0003_alter_tenant_default_token_duration"), | ||||||
|  |     ] | ||||||
|  |  | ||||||
|  |     operations = [ | ||||||
|  |         migrations.AddField( | ||||||
|  |             model_name="tenant", | ||||||
|  |             name="analytics_enabled", | ||||||
|  |             field=models.BooleanField(default=False), | ||||||
|  |         ), | ||||||
|  |         migrations.AddField( | ||||||
|  |             model_name="tenant", | ||||||
|  |             name="analytics_sources", | ||||||
|  |             field=django.contrib.postgres.fields.ArrayField( | ||||||
|  |                 base_field=models.TextField(), blank=True, default=list, size=None | ||||||
|  |             ), | ||||||
|  |         ), | ||||||
|  |     ] | ||||||
| @ -4,6 +4,7 @@ import re | |||||||
| from uuid import uuid4 | from uuid import uuid4 | ||||||
|  |  | ||||||
| from django.apps import apps | from django.apps import apps | ||||||
|  | from django.contrib.postgres.fields import ArrayField | ||||||
| from django.core.exceptions import ValidationError | from django.core.exceptions import ValidationError | ||||||
| from django.core.validators import MinValueValidator | from django.core.validators import MinValueValidator | ||||||
| from django.db import models | from django.db import models | ||||||
| @ -96,6 +97,9 @@ class Tenant(TenantMixin, SerializerModel): | |||||||
|         validators=[MinValueValidator(1)], |         validators=[MinValueValidator(1)], | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|  |     analytics_enabled = models.BooleanField(default=False) | ||||||
|  |     analytics_sources = ArrayField(models.TextField(), blank=True, default=list) | ||||||
|  |  | ||||||
|     def save(self, *args, **kwargs): |     def save(self, *args, **kwargs): | ||||||
|         if self.schema_name == "template": |         if self.schema_name == "template": | ||||||
|             raise IntegrityError("Cannot create schema named template") |             raise IntegrityError("Cannot create schema named template") | ||||||
|  | |||||||
| @ -4227,6 +4227,7 @@ | |||||||
|                         "model_updated", |                         "model_updated", | ||||||
|                         "model_deleted", |                         "model_deleted", | ||||||
|                         "email_sent", |                         "email_sent", | ||||||
|  |                         "analytics_sent", | ||||||
|                         "update_available", |                         "update_available", | ||||||
|                         "custom_" |                         "custom_" | ||||||
|                     ], |                     ], | ||||||
| @ -4251,6 +4252,7 @@ | |||||||
|                         null, |                         null, | ||||||
|                         "authentik.tenants", |                         "authentik.tenants", | ||||||
|                         "authentik.admin", |                         "authentik.admin", | ||||||
|  |                         "authentik.analytics", | ||||||
|                         "authentik.api", |                         "authentik.api", | ||||||
|                         "authentik.crypto", |                         "authentik.crypto", | ||||||
|                         "authentik.flows", |                         "authentik.flows", | ||||||
| @ -13116,6 +13118,7 @@ | |||||||
|                         "model_updated", |                         "model_updated", | ||||||
|                         "model_deleted", |                         "model_deleted", | ||||||
|                         "email_sent", |                         "email_sent", | ||||||
|  |                         "analytics_sent", | ||||||
|                         "update_available", |                         "update_available", | ||||||
|                         "custom_" |                         "custom_" | ||||||
|                     ], |                     ], | ||||||
| @ -13277,6 +13280,7 @@ | |||||||
|                                 "model_updated", |                                 "model_updated", | ||||||
|                                 "model_deleted", |                                 "model_deleted", | ||||||
|                                 "email_sent", |                                 "email_sent", | ||||||
|  |                                 "analytics_sent", | ||||||
|                                 "update_available", |                                 "update_available", | ||||||
|                                 "custom_" |                                 "custom_" | ||||||
|                             ], |                             ], | ||||||
|  | |||||||
							
								
								
									
										111
									
								
								schema.yml
									
									
									
									
									
								
							
							
						
						
									
										111
									
								
								schema.yml
									
									
									
									
									
								
							| @ -290,6 +290,64 @@ paths: | |||||||
|               schema: |               schema: | ||||||
|                 $ref: '#/components/schemas/GenericError' |                 $ref: '#/components/schemas/GenericError' | ||||||
|           description: '' |           description: '' | ||||||
|  |   /analytics/data/: | ||||||
|  |     get: | ||||||
|  |       operationId: analytics_data_list | ||||||
|  |       description: Read-only view of analytics descriptions | ||||||
|  |       tags: | ||||||
|  |       - analytics | ||||||
|  |       security: | ||||||
|  |       - authentik: [] | ||||||
|  |       responses: | ||||||
|  |         '200': | ||||||
|  |           content: | ||||||
|  |             application/json: | ||||||
|  |               schema: | ||||||
|  |                 type: array | ||||||
|  |                 items: | ||||||
|  |                   $ref: '#/components/schemas/AnalyticsData' | ||||||
|  |           description: '' | ||||||
|  |         '400': | ||||||
|  |           content: | ||||||
|  |             application/json: | ||||||
|  |               schema: | ||||||
|  |                 $ref: '#/components/schemas/ValidationError' | ||||||
|  |           description: '' | ||||||
|  |         '403': | ||||||
|  |           content: | ||||||
|  |             application/json: | ||||||
|  |               schema: | ||||||
|  |                 $ref: '#/components/schemas/GenericError' | ||||||
|  |           description: '' | ||||||
|  |   /analytics/description/: | ||||||
|  |     get: | ||||||
|  |       operationId: analytics_description_list | ||||||
|  |       description: Read-only view of analytics descriptions | ||||||
|  |       tags: | ||||||
|  |       - analytics | ||||||
|  |       security: | ||||||
|  |       - authentik: [] | ||||||
|  |       responses: | ||||||
|  |         '200': | ||||||
|  |           content: | ||||||
|  |             application/json: | ||||||
|  |               schema: | ||||||
|  |                 type: array | ||||||
|  |                 items: | ||||||
|  |                   $ref: '#/components/schemas/AnalyticsDescription' | ||||||
|  |           description: '' | ||||||
|  |         '400': | ||||||
|  |           content: | ||||||
|  |             application/json: | ||||||
|  |               schema: | ||||||
|  |                 $ref: '#/components/schemas/ValidationError' | ||||||
|  |           description: '' | ||||||
|  |         '403': | ||||||
|  |           content: | ||||||
|  |             application/json: | ||||||
|  |               schema: | ||||||
|  |                 $ref: '#/components/schemas/GenericError' | ||||||
|  |           description: '' | ||||||
|   /authenticators/admin/all/: |   /authenticators/admin/all/: | ||||||
|     get: |     get: | ||||||
|       operationId: authenticators_admin_all_list |       operationId: authenticators_admin_all_list | ||||||
| @ -11502,6 +11560,7 @@ paths: | |||||||
|           type: string |           type: string | ||||||
|           nullable: true |           nullable: true | ||||||
|           enum: |           enum: | ||||||
|  |           - analytics_sent | ||||||
|           - authorize_application |           - authorize_application | ||||||
|           - configuration_error |           - configuration_error | ||||||
|           - custom_ |           - custom_ | ||||||
| @ -35758,6 +35817,25 @@ components: | |||||||
|       - rsa |       - rsa | ||||||
|       - ecdsa |       - ecdsa | ||||||
|       type: string |       type: string | ||||||
|  |     AnalyticsData: | ||||||
|  |       type: object | ||||||
|  |       properties: | ||||||
|  |         data: | ||||||
|  |           type: object | ||||||
|  |           additionalProperties: {} | ||||||
|  |       required: | ||||||
|  |       - data | ||||||
|  |     AnalyticsDescription: | ||||||
|  |       type: object | ||||||
|  |       description: Base serializer class which doesn't implement create/update methods | ||||||
|  |       properties: | ||||||
|  |         label: | ||||||
|  |           type: string | ||||||
|  |         desc: | ||||||
|  |           type: string | ||||||
|  |       required: | ||||||
|  |       - desc | ||||||
|  |       - label | ||||||
|     App: |     App: | ||||||
|       type: object |       type: object | ||||||
|       description: Serialize Application info |       description: Serialize Application info | ||||||
| @ -35773,6 +35851,7 @@ components: | |||||||
|       enum: |       enum: | ||||||
|       - authentik.tenants |       - authentik.tenants | ||||||
|       - authentik.admin |       - authentik.admin | ||||||
|  |       - authentik.analytics | ||||||
|       - authentik.api |       - authentik.api | ||||||
|       - authentik.crypto |       - authentik.crypto | ||||||
|       - authentik.flows |       - authentik.flows | ||||||
| @ -38960,6 +39039,7 @@ components: | |||||||
|       - model_updated |       - model_updated | ||||||
|       - model_deleted |       - model_deleted | ||||||
|       - email_sent |       - email_sent | ||||||
|  |       - analytics_sent | ||||||
|       - update_available |       - update_available | ||||||
|       - custom_ |       - custom_ | ||||||
|       type: string |       type: string | ||||||
| @ -47361,6 +47441,13 @@ components: | |||||||
|           maximum: 2147483647 |           maximum: 2147483647 | ||||||
|           minimum: 1 |           minimum: 1 | ||||||
|           description: Default token length |           description: Default token length | ||||||
|  |         analytics_enabled: | ||||||
|  |           type: boolean | ||||||
|  |         analytics_sources: | ||||||
|  |           type: array | ||||||
|  |           items: | ||||||
|  |             type: string | ||||||
|  |             minLength: 1 | ||||||
|     PatchedSourceStageRequest: |     PatchedSourceStageRequest: | ||||||
|       type: object |       type: object | ||||||
|       description: SourceStage Serializer |       description: SourceStage Serializer | ||||||
| @ -51001,6 +51088,23 @@ components: | |||||||
|           maximum: 2147483647 |           maximum: 2147483647 | ||||||
|           minimum: 1 |           minimum: 1 | ||||||
|           description: Default token length |           description: Default token length | ||||||
|  |         analytics_enabled: | ||||||
|  |           type: boolean | ||||||
|  |         analytics_sources: | ||||||
|  |           type: array | ||||||
|  |           items: | ||||||
|  |             type: string | ||||||
|  |         analytics_sources_obj: | ||||||
|  |           type: array | ||||||
|  |           items: | ||||||
|  |             type: array | ||||||
|  |             items: | ||||||
|  |               type: string | ||||||
|  |             minLength: 2 | ||||||
|  |             maxLength: 2 | ||||||
|  |           readOnly: true | ||||||
|  |       required: | ||||||
|  |       - analytics_sources_obj | ||||||
|     SettingsRequest: |     SettingsRequest: | ||||||
|       type: object |       type: object | ||||||
|       description: Settings Serializer |       description: Settings Serializer | ||||||
| @ -51041,6 +51145,13 @@ components: | |||||||
|           maximum: 2147483647 |           maximum: 2147483647 | ||||||
|           minimum: 1 |           minimum: 1 | ||||||
|           description: Default token length |           description: Default token length | ||||||
|  |         analytics_enabled: | ||||||
|  |           type: boolean | ||||||
|  |         analytics_sources: | ||||||
|  |           type: array | ||||||
|  |           items: | ||||||
|  |             type: string | ||||||
|  |             minLength: 1 | ||||||
|     SeverityEnum: |     SeverityEnum: | ||||||
|       enum: |       enum: | ||||||
|       - notice |       - notice | ||||||
|  | |||||||
| @ -5,6 +5,8 @@ import "@goauthentik/components/ak-switch-input"; | |||||||
| import "@goauthentik/components/ak-text-input"; | import "@goauthentik/components/ak-text-input"; | ||||||
| import "@goauthentik/elements/CodeMirror"; | import "@goauthentik/elements/CodeMirror"; | ||||||
| import { CodeMirrorMode } from "@goauthentik/elements/CodeMirror"; | import { CodeMirrorMode } from "@goauthentik/elements/CodeMirror"; | ||||||
|  | import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js"; | ||||||
|  | import { DataProvision, DualSelectPair } from "@goauthentik/elements/ak-dual-select/types"; | ||||||
| import { Form } from "@goauthentik/elements/forms/Form"; | import { Form } from "@goauthentik/elements/forms/Form"; | ||||||
| import "@goauthentik/elements/forms/FormGroup"; | import "@goauthentik/elements/forms/FormGroup"; | ||||||
| import "@goauthentik/elements/forms/HorizontalFormElement"; | import "@goauthentik/elements/forms/HorizontalFormElement"; | ||||||
| @ -19,7 +21,17 @@ import { ifDefined } from "lit/directives/if-defined.js"; | |||||||
|  |  | ||||||
| import PFList from "@patternfly/patternfly/components/List/list.css"; | import PFList from "@patternfly/patternfly/components/List/list.css"; | ||||||
|  |  | ||||||
| import { AdminApi, Settings, SettingsRequest } from "@goauthentik/api"; | import { | ||||||
|  |     AdminApi, | ||||||
|  |     AnalyticsApi, | ||||||
|  |     AnalyticsDescription, | ||||||
|  |     Settings, | ||||||
|  |     SettingsRequest, | ||||||
|  | } from "@goauthentik/api"; | ||||||
|  |  | ||||||
|  | function analyticsSourceToPair(analyticsSource: AnalyticsDescription): DualSelectPair { | ||||||
|  |     return [analyticsSource.label, analyticsSource.desc]; | ||||||
|  | } | ||||||
|  |  | ||||||
| @customElement("ak-admin-settings-form") | @customElement("ak-admin-settings-form") | ||||||
| export class AdminSettingsForm extends Form<SettingsRequest> { | export class AdminSettingsForm extends Form<SettingsRequest> { | ||||||
| @ -211,6 +223,42 @@ export class AdminSettingsForm extends Form<SettingsRequest> { | |||||||
|                 value="${first(this._settings?.defaultTokenLength, 60)}" |                 value="${first(this._settings?.defaultTokenLength, 60)}" | ||||||
|                 help=${msg("Default length of generated tokens")} |                 help=${msg("Default length of generated tokens")} | ||||||
|             ></ak-number-input> |             ></ak-number-input> | ||||||
|  |             <ak-form-group ?expanded=${false}> | ||||||
|  |                 <span slot="header"> ${msg("Analytics")} </span> | ||||||
|  |                 <div slot="body" class="pf-c-form"> | ||||||
|  |                     <ak-switch-input | ||||||
|  |                         name="analyticsEnabled" | ||||||
|  |                         label=${msg("Enable analytics")} | ||||||
|  |                         ?checked="${this._settings?.analyticsEnabled}" | ||||||
|  |                         help=${msg("Enable sending analytics to the authentik team")} | ||||||
|  |                     > | ||||||
|  |                     </ak-switch-input> | ||||||
|  |                     <ak-form-element-horizontal | ||||||
|  |                         label=${msg("Analytics sources")} | ||||||
|  |                         name="analyticsSources" | ||||||
|  |                     > | ||||||
|  |                         <ak-dual-select-provider | ||||||
|  |                             .provider=${async ( | ||||||
|  |                                 _page: number, | ||||||
|  |                                 _search?: string, | ||||||
|  |                             ): Promise<DataProvision> => { | ||||||
|  |                                 return new AnalyticsApi(DEFAULT_CONFIG) | ||||||
|  |                                     .analyticsDescriptionList() | ||||||
|  |                                     .then((results) => { | ||||||
|  |                                         return { | ||||||
|  |                                             options: results.map(analyticsSourceToPair), | ||||||
|  |                                         }; | ||||||
|  |                                     }); | ||||||
|  |                             }} | ||||||
|  |                             .selected=${this._settings?.analyticsSourcesObj ?? []} | ||||||
|  |                             available-label="${msg("Available sources")}" | ||||||
|  |                             selected-label="${msg("Selected sources")}" | ||||||
|  |                         > | ||||||
|  |                         </ak-dual-select-provider> | ||||||
|  |                         <p class="pf-c-form__helper-text">${msg("Choose what data to send")}</p> | ||||||
|  |                     </ak-form-element-horizontal> | ||||||
|  |                 </div> | ||||||
|  |             </ak-form-group> | ||||||
|         `; |         `; | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | |||||||
		Reference in New Issue
	
	Block a user
	