Compare commits

...

3 Commits

Author SHA1 Message Date
435ba598bb add tests
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-09-25 15:07:00 +02:00
582511abcc add version
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-09-25 14:42:24 +02:00
80ea1dae81 analytics: init
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-09-25 14:37:27 +02:00
21 changed files with 685 additions and 9 deletions

View 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,
}

View File

View 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),
}
)

View 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

View 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()}

View 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"},
}
}

View 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)

View 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())

View 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"),
]

View 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(),
}

View File

@ -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)

View 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"),
]
),
),
]

View File

@ -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_"

View File

@ -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,
),
),
]

View File

@ -70,6 +70,7 @@ TENANT_APPS = [
"django.contrib.contenttypes",
"django.contrib.sessions",
"authentik.admin",
"authentik.analytics",
"authentik.api",
"authentik.crypto",
"authentik.flows",

View File

@ -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"""

View File

@ -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
),
),
]

View File

@ -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")

View File

@ -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_"
],

View File

@ -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

View File

@ -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>
`;
}
}