Compare commits
	
		
			3 Commits
		
	
	
		
			enterprise
			...
			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 structlog.stdlib import get_logger
 | 
			
		||||
 | 
			
		||||
from authentik.analytics.models import AnalyticsMixin
 | 
			
		||||
from authentik.blueprints.models import ManagedModel
 | 
			
		||||
from authentik.core.expression.exceptions import PropertyMappingExpressionException
 | 
			
		||||
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)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Group(SerializerModel, AttributesMixin):
 | 
			
		||||
class Group(SerializerModel, AttributesMixin, AnalyticsMixin):
 | 
			
		||||
    """Group model which supports a basic hierarchy and has attributes"""
 | 
			
		||||
 | 
			
		||||
    group_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4)
 | 
			
		||||
@ -258,7 +259,7 @@ class UserManager(DjangoUserManager):
 | 
			
		||||
        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."""
 | 
			
		||||
 | 
			
		||||
    uuid = models.UUIDField(default=uuid4, editable=False, unique=True)
 | 
			
		||||
@ -376,7 +377,7 @@ class User(SerializerModel, GuardianUserMixin, AttributesMixin, AbstractUser):
 | 
			
		||||
        return get_avatar(self)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Provider(SerializerModel):
 | 
			
		||||
class Provider(SerializerModel, AnalyticsMixin):
 | 
			
		||||
    """Application-independent Provider instance. For example SAML2 Remote, OAuth2 Application"""
 | 
			
		||||
 | 
			
		||||
    name = models.TextField(unique=True)
 | 
			
		||||
@ -470,7 +471,7 @@ class ApplicationQuerySet(QuerySet):
 | 
			
		||||
        return qs
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Application(SerializerModel, PolicyBindingModel):
 | 
			
		||||
class Application(SerializerModel, PolicyBindingModel, AnalyticsMixin):
 | 
			
		||||
    """Every Application which uses authentik for authentication/identification/authorization
 | 
			
		||||
    needs an Application record. Other authentication types can subclass this Model to
 | 
			
		||||
    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"""
 | 
			
		||||
 | 
			
		||||
    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."""
 | 
			
		||||
 | 
			
		||||
    user = models.ForeignKey(User, on_delete=models.CASCADE)
 | 
			
		||||
@ -755,7 +756,7 @@ class UserSourceConnection(SerializerModel, CreatedUpdatedModel):
 | 
			
		||||
        unique_together = (("user", "source"),)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class GroupSourceConnection(SerializerModel, CreatedUpdatedModel):
 | 
			
		||||
class GroupSourceConnection(SerializerModel, CreatedUpdatedModel, AnalyticsMixin):
 | 
			
		||||
    """Connection between Group and Source."""
 | 
			
		||||
 | 
			
		||||
    group = models.ForeignKey(Group, on_delete=models.CASCADE)
 | 
			
		||||
@ -879,7 +880,7 @@ class Token(SerializerModel, ManagedModel, ExpiringModel):
 | 
			
		||||
        ).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."""
 | 
			
		||||
 | 
			
		||||
    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"
 | 
			
		||||
    EMAIL_SENT = "email_sent"
 | 
			
		||||
 | 
			
		||||
    ANALYTICS_SENT = "analytics_sent"
 | 
			
		||||
    UPDATE_AVAILABLE = "update_available"
 | 
			
		||||
 | 
			
		||||
    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.sessions",
 | 
			
		||||
    "authentik.admin",
 | 
			
		||||
    "authentik.analytics",
 | 
			
		||||
    "authentik.api",
 | 
			
		||||
    "authentik.crypto",
 | 
			
		||||
    "authentik.flows",
 | 
			
		||||
 | 
			
		||||
@ -1,9 +1,12 @@
 | 
			
		||||
"""Serializer for tenants models"""
 | 
			
		||||
 | 
			
		||||
from django_tenants.utils import get_public_schema_name
 | 
			
		||||
from rest_framework.fields import SerializerMethodField
 | 
			
		||||
from rest_framework.generics import RetrieveUpdateAPIView
 | 
			
		||||
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.rbac.permissions import HasPermission
 | 
			
		||||
from authentik.tenants.models import Tenant
 | 
			
		||||
@ -12,6 +15,8 @@ from authentik.tenants.models import Tenant
 | 
			
		||||
class SettingsSerializer(ModelSerializer):
 | 
			
		||||
    """Settings Serializer"""
 | 
			
		||||
 | 
			
		||||
    analytics_sources_obj = SerializerMethodField()
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Tenant
 | 
			
		||||
        fields = [
 | 
			
		||||
@ -25,8 +30,19 @@ class SettingsSerializer(ModelSerializer):
 | 
			
		||||
            "impersonation",
 | 
			
		||||
            "default_token_duration",
 | 
			
		||||
            "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):
 | 
			
		||||
    """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 django.apps import apps
 | 
			
		||||
from django.contrib.postgres.fields import ArrayField
 | 
			
		||||
from django.core.exceptions import ValidationError
 | 
			
		||||
from django.core.validators import MinValueValidator
 | 
			
		||||
from django.db import models
 | 
			
		||||
@ -96,6 +97,9 @@ class Tenant(TenantMixin, SerializerModel):
 | 
			
		||||
        validators=[MinValueValidator(1)],
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    analytics_enabled = models.BooleanField(default=False)
 | 
			
		||||
    analytics_sources = ArrayField(models.TextField(), blank=True, default=list)
 | 
			
		||||
 | 
			
		||||
    def save(self, *args, **kwargs):
 | 
			
		||||
        if self.schema_name == "template":
 | 
			
		||||
            raise IntegrityError("Cannot create schema named template")
 | 
			
		||||
 | 
			
		||||
@ -4227,6 +4227,7 @@
 | 
			
		||||
                        "model_updated",
 | 
			
		||||
                        "model_deleted",
 | 
			
		||||
                        "email_sent",
 | 
			
		||||
                        "analytics_sent",
 | 
			
		||||
                        "update_available",
 | 
			
		||||
                        "custom_"
 | 
			
		||||
                    ],
 | 
			
		||||
@ -4251,6 +4252,7 @@
 | 
			
		||||
                        null,
 | 
			
		||||
                        "authentik.tenants",
 | 
			
		||||
                        "authentik.admin",
 | 
			
		||||
                        "authentik.analytics",
 | 
			
		||||
                        "authentik.api",
 | 
			
		||||
                        "authentik.crypto",
 | 
			
		||||
                        "authentik.flows",
 | 
			
		||||
@ -13116,6 +13118,7 @@
 | 
			
		||||
                        "model_updated",
 | 
			
		||||
                        "model_deleted",
 | 
			
		||||
                        "email_sent",
 | 
			
		||||
                        "analytics_sent",
 | 
			
		||||
                        "update_available",
 | 
			
		||||
                        "custom_"
 | 
			
		||||
                    ],
 | 
			
		||||
@ -13277,6 +13280,7 @@
 | 
			
		||||
                                "model_updated",
 | 
			
		||||
                                "model_deleted",
 | 
			
		||||
                                "email_sent",
 | 
			
		||||
                                "analytics_sent",
 | 
			
		||||
                                "update_available",
 | 
			
		||||
                                "custom_"
 | 
			
		||||
                            ],
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										111
									
								
								schema.yml
									
									
									
									
									
								
							
							
						
						
									
										111
									
								
								schema.yml
									
									
									
									
									
								
							@ -290,6 +290,64 @@ paths:
 | 
			
		||||
              schema:
 | 
			
		||||
                $ref: '#/components/schemas/GenericError'
 | 
			
		||||
          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/:
 | 
			
		||||
    get:
 | 
			
		||||
      operationId: authenticators_admin_all_list
 | 
			
		||||
@ -11502,6 +11560,7 @@ paths:
 | 
			
		||||
          type: string
 | 
			
		||||
          nullable: true
 | 
			
		||||
          enum:
 | 
			
		||||
          - analytics_sent
 | 
			
		||||
          - authorize_application
 | 
			
		||||
          - configuration_error
 | 
			
		||||
          - custom_
 | 
			
		||||
@ -35758,6 +35817,25 @@ components:
 | 
			
		||||
      - rsa
 | 
			
		||||
      - ecdsa
 | 
			
		||||
      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:
 | 
			
		||||
      type: object
 | 
			
		||||
      description: Serialize Application info
 | 
			
		||||
@ -35773,6 +35851,7 @@ components:
 | 
			
		||||
      enum:
 | 
			
		||||
      - authentik.tenants
 | 
			
		||||
      - authentik.admin
 | 
			
		||||
      - authentik.analytics
 | 
			
		||||
      - authentik.api
 | 
			
		||||
      - authentik.crypto
 | 
			
		||||
      - authentik.flows
 | 
			
		||||
@ -38960,6 +39039,7 @@ components:
 | 
			
		||||
      - model_updated
 | 
			
		||||
      - model_deleted
 | 
			
		||||
      - email_sent
 | 
			
		||||
      - analytics_sent
 | 
			
		||||
      - update_available
 | 
			
		||||
      - custom_
 | 
			
		||||
      type: string
 | 
			
		||||
@ -47361,6 +47441,13 @@ components:
 | 
			
		||||
          maximum: 2147483647
 | 
			
		||||
          minimum: 1
 | 
			
		||||
          description: Default token length
 | 
			
		||||
        analytics_enabled:
 | 
			
		||||
          type: boolean
 | 
			
		||||
        analytics_sources:
 | 
			
		||||
          type: array
 | 
			
		||||
          items:
 | 
			
		||||
            type: string
 | 
			
		||||
            minLength: 1
 | 
			
		||||
    PatchedSourceStageRequest:
 | 
			
		||||
      type: object
 | 
			
		||||
      description: SourceStage Serializer
 | 
			
		||||
@ -51001,6 +51088,23 @@ components:
 | 
			
		||||
          maximum: 2147483647
 | 
			
		||||
          minimum: 1
 | 
			
		||||
          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:
 | 
			
		||||
      type: object
 | 
			
		||||
      description: Settings Serializer
 | 
			
		||||
@ -51041,6 +51145,13 @@ components:
 | 
			
		||||
          maximum: 2147483647
 | 
			
		||||
          minimum: 1
 | 
			
		||||
          description: Default token length
 | 
			
		||||
        analytics_enabled:
 | 
			
		||||
          type: boolean
 | 
			
		||||
        analytics_sources:
 | 
			
		||||
          type: array
 | 
			
		||||
          items:
 | 
			
		||||
            type: string
 | 
			
		||||
            minLength: 1
 | 
			
		||||
    SeverityEnum:
 | 
			
		||||
      enum:
 | 
			
		||||
      - notice
 | 
			
		||||
 | 
			
		||||
@ -5,6 +5,8 @@ import "@goauthentik/components/ak-switch-input";
 | 
			
		||||
import "@goauthentik/components/ak-text-input";
 | 
			
		||||
import "@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 "@goauthentik/elements/forms/FormGroup";
 | 
			
		||||
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 { 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")
 | 
			
		||||
export class AdminSettingsForm extends Form<SettingsRequest> {
 | 
			
		||||
@ -211,6 +223,42 @@ export class AdminSettingsForm extends Form<SettingsRequest> {
 | 
			
		||||
                value="${first(this._settings?.defaultTokenLength, 60)}"
 | 
			
		||||
                help=${msg("Default length of generated tokens")}
 | 
			
		||||
            ></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