Compare commits
	
		
			2 Commits
		
	
	
		
			safari-adm
			...
			policies-e
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| ad9b5e98ba | |||
| e4a21c824a | 
| @ -94,7 +94,7 @@ RUN --mount=type=secret,id=GEOIPUPDATE_ACCOUNT_ID \ | |||||||
|     /bin/sh -c "/usr/bin/entry.sh || echo 'Failed to get GeoIP database, disabling'; exit 0" |     /bin/sh -c "/usr/bin/entry.sh || echo 'Failed to get GeoIP database, disabling'; exit 0" | ||||||
|  |  | ||||||
| # Stage 5: Download uv | # Stage 5: Download uv | ||||||
| FROM ghcr.io/astral-sh/uv:0.6.16 AS uv | FROM ghcr.io/astral-sh/uv:0.6.14 AS uv | ||||||
| # Stage 6: Base python image | # Stage 6: Base python image | ||||||
| FROM ghcr.io/goauthentik/fips-python:3.12.10-slim-bookworm-fips AS python-base | FROM ghcr.io/goauthentik/fips-python:3.12.10-slim-bookworm-fips AS python-base | ||||||
|  |  | ||||||
|  | |||||||
| @ -13,10 +13,7 @@ from authentik.core.models import ( | |||||||
|     TokenIntents, |     TokenIntents, | ||||||
|     User, |     User, | ||||||
| ) | ) | ||||||
| from authentik.core.tasks import ( | from authentik.core.tasks import clean_expired_models, clean_temporary_users | ||||||
|     clean_expired_models, |  | ||||||
|     clean_temporary_users, |  | ||||||
| ) |  | ||||||
| from authentik.core.tests.utils import create_test_admin_user | from authentik.core.tests.utils import create_test_admin_user | ||||||
| from authentik.lib.generators import generate_id | from authentik.lib.generators import generate_id | ||||||
|  |  | ||||||
|  | |||||||
| @ -1,27 +0,0 @@ | |||||||
| from rest_framework.viewsets import ModelViewSet |  | ||||||
|  |  | ||||||
| from authentik.core.api.used_by import UsedByMixin |  | ||||||
| from authentik.enterprise.api import EnterpriseRequiredMixin |  | ||||||
| from authentik.enterprise.policies.unique_password.models import UniquePasswordPolicy |  | ||||||
| from authentik.policies.api.policies import PolicySerializer |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class UniquePasswordPolicySerializer(EnterpriseRequiredMixin, PolicySerializer): |  | ||||||
|     """Password Uniqueness Policy Serializer""" |  | ||||||
|  |  | ||||||
|     class Meta: |  | ||||||
|         model = UniquePasswordPolicy |  | ||||||
|         fields = PolicySerializer.Meta.fields + [ |  | ||||||
|             "password_field", |  | ||||||
|             "num_historical_passwords", |  | ||||||
|         ] |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class UniquePasswordPolicyViewSet(UsedByMixin, ModelViewSet): |  | ||||||
|     """Password Uniqueness Policy Viewset""" |  | ||||||
|  |  | ||||||
|     queryset = UniquePasswordPolicy.objects.all() |  | ||||||
|     serializer_class = UniquePasswordPolicySerializer |  | ||||||
|     filterset_fields = "__all__" |  | ||||||
|     ordering = ["name"] |  | ||||||
|     search_fields = ["name"] |  | ||||||
| @ -1,10 +0,0 @@ | |||||||
| """authentik Unique Password policy app config""" |  | ||||||
|  |  | ||||||
| from authentik.enterprise.apps import EnterpriseConfig |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class AuthentikEnterprisePoliciesUniquePasswordConfig(EnterpriseConfig): |  | ||||||
|     name = "authentik.enterprise.policies.unique_password" |  | ||||||
|     label = "authentik_policies_unique_password" |  | ||||||
|     verbose_name = "authentik Enterprise.Policies.Unique Password" |  | ||||||
|     default = True |  | ||||||
| @ -1,81 +0,0 @@ | |||||||
| # Generated by Django 5.0.13 on 2025-03-26 23:02 |  | ||||||
|  |  | ||||||
| import django.db.models.deletion |  | ||||||
| from django.conf import settings |  | ||||||
| from django.db import migrations, models |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class Migration(migrations.Migration): |  | ||||||
|  |  | ||||||
|     initial = True |  | ||||||
|  |  | ||||||
|     dependencies = [ |  | ||||||
|         ("authentik_policies", "0011_policybinding_failure_result_and_more"), |  | ||||||
|         migrations.swappable_dependency(settings.AUTH_USER_MODEL), |  | ||||||
|     ] |  | ||||||
|  |  | ||||||
|     operations = [ |  | ||||||
|         migrations.CreateModel( |  | ||||||
|             name="UniquePasswordPolicy", |  | ||||||
|             fields=[ |  | ||||||
|                 ( |  | ||||||
|                     "policy_ptr", |  | ||||||
|                     models.OneToOneField( |  | ||||||
|                         auto_created=True, |  | ||||||
|                         on_delete=django.db.models.deletion.CASCADE, |  | ||||||
|                         parent_link=True, |  | ||||||
|                         primary_key=True, |  | ||||||
|                         serialize=False, |  | ||||||
|                         to="authentik_policies.policy", |  | ||||||
|                     ), |  | ||||||
|                 ), |  | ||||||
|                 ( |  | ||||||
|                     "password_field", |  | ||||||
|                     models.TextField( |  | ||||||
|                         default="password", |  | ||||||
|                         help_text="Field key to check, field keys defined in Prompt stages are available.", |  | ||||||
|                     ), |  | ||||||
|                 ), |  | ||||||
|                 ( |  | ||||||
|                     "num_historical_passwords", |  | ||||||
|                     models.PositiveIntegerField( |  | ||||||
|                         default=1, help_text="Number of passwords to check against." |  | ||||||
|                     ), |  | ||||||
|                 ), |  | ||||||
|             ], |  | ||||||
|             options={ |  | ||||||
|                 "verbose_name": "Password Uniqueness Policy", |  | ||||||
|                 "verbose_name_plural": "Password Uniqueness Policies", |  | ||||||
|                 "indexes": [ |  | ||||||
|                     models.Index(fields=["policy_ptr_id"], name="authentik_p_policy__f559aa_idx") |  | ||||||
|                 ], |  | ||||||
|             }, |  | ||||||
|             bases=("authentik_policies.policy",), |  | ||||||
|         ), |  | ||||||
|         migrations.CreateModel( |  | ||||||
|             name="UserPasswordHistory", |  | ||||||
|             fields=[ |  | ||||||
|                 ( |  | ||||||
|                     "id", |  | ||||||
|                     models.AutoField( |  | ||||||
|                         auto_created=True, primary_key=True, serialize=False, verbose_name="ID" |  | ||||||
|                     ), |  | ||||||
|                 ), |  | ||||||
|                 ("old_password", models.CharField(max_length=128)), |  | ||||||
|                 ("created_at", models.DateTimeField(auto_now_add=True)), |  | ||||||
|                 ("hibp_prefix_sha1", models.CharField(max_length=5)), |  | ||||||
|                 ("hibp_pw_hash", models.TextField()), |  | ||||||
|                 ( |  | ||||||
|                     "user", |  | ||||||
|                     models.ForeignKey( |  | ||||||
|                         on_delete=django.db.models.deletion.CASCADE, |  | ||||||
|                         related_name="old_passwords", |  | ||||||
|                         to=settings.AUTH_USER_MODEL, |  | ||||||
|                     ), |  | ||||||
|                 ), |  | ||||||
|             ], |  | ||||||
|             options={ |  | ||||||
|                 "verbose_name": "User Password History", |  | ||||||
|             }, |  | ||||||
|         ), |  | ||||||
|     ] |  | ||||||
| @ -1,151 +0,0 @@ | |||||||
| from hashlib import sha1 |  | ||||||
|  |  | ||||||
| from django.contrib.auth.hashers import identify_hasher, make_password |  | ||||||
| from django.db import models |  | ||||||
| from django.utils.translation import gettext as _ |  | ||||||
| from rest_framework.serializers import BaseSerializer |  | ||||||
| from structlog.stdlib import get_logger |  | ||||||
|  |  | ||||||
| from authentik.core.models import User |  | ||||||
| from authentik.policies.models import Policy |  | ||||||
| from authentik.policies.types import PolicyRequest, PolicyResult |  | ||||||
| from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT |  | ||||||
|  |  | ||||||
| LOGGER = get_logger() |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class UniquePasswordPolicy(Policy): |  | ||||||
|     """This policy prevents users from reusing old passwords.""" |  | ||||||
|  |  | ||||||
|     password_field = models.TextField( |  | ||||||
|         default="password", |  | ||||||
|         help_text=_("Field key to check, field keys defined in Prompt stages are available."), |  | ||||||
|     ) |  | ||||||
|  |  | ||||||
|     # Limit on the number of previous passwords the policy evaluates |  | ||||||
|     # Also controls number of old passwords the system stores. |  | ||||||
|     num_historical_passwords = models.PositiveIntegerField( |  | ||||||
|         default=1, |  | ||||||
|         help_text=_("Number of passwords to check against."), |  | ||||||
|     ) |  | ||||||
|  |  | ||||||
|     @property |  | ||||||
|     def serializer(self) -> type[BaseSerializer]: |  | ||||||
|         from authentik.enterprise.policies.unique_password.api import UniquePasswordPolicySerializer |  | ||||||
|  |  | ||||||
|         return UniquePasswordPolicySerializer |  | ||||||
|  |  | ||||||
|     @property |  | ||||||
|     def component(self) -> str: |  | ||||||
|         return "ak-policy-password-uniqueness-form" |  | ||||||
|  |  | ||||||
|     def passes(self, request: PolicyRequest) -> PolicyResult: |  | ||||||
|         from authentik.enterprise.policies.unique_password.models import UserPasswordHistory |  | ||||||
|  |  | ||||||
|         password = request.context.get(PLAN_CONTEXT_PROMPT, {}).get( |  | ||||||
|             self.password_field, request.context.get(self.password_field) |  | ||||||
|         ) |  | ||||||
|         if not password: |  | ||||||
|             LOGGER.warning( |  | ||||||
|                 "Password field not found in request when checking UniquePasswordPolicy", |  | ||||||
|                 field=self.password_field, |  | ||||||
|                 fields=request.context.keys(), |  | ||||||
|             ) |  | ||||||
|             return PolicyResult(False, _("Password not set in context")) |  | ||||||
|         password = str(password) |  | ||||||
|  |  | ||||||
|         if not self.num_historical_passwords: |  | ||||||
|             # Policy not configured to check against any passwords |  | ||||||
|             return PolicyResult(True) |  | ||||||
|  |  | ||||||
|         num_to_check = self.num_historical_passwords |  | ||||||
|         password_history = UserPasswordHistory.objects.filter(user=request.user).order_by( |  | ||||||
|             "-created_at" |  | ||||||
|         )[:num_to_check] |  | ||||||
|  |  | ||||||
|         if not password_history: |  | ||||||
|             return PolicyResult(True) |  | ||||||
|  |  | ||||||
|         for record in password_history: |  | ||||||
|             if not record.old_password: |  | ||||||
|                 continue |  | ||||||
|  |  | ||||||
|             if self._passwords_match(new_password=password, old_password=record.old_password): |  | ||||||
|                 # Return on first match. Authentik does not consider timing attacks |  | ||||||
|                 # on old passwords to be an attack surface. |  | ||||||
|                 return PolicyResult( |  | ||||||
|                     False, |  | ||||||
|                     _("This password has been used previously. Please choose a different one."), |  | ||||||
|                 ) |  | ||||||
|  |  | ||||||
|         return PolicyResult(True) |  | ||||||
|  |  | ||||||
|     def _passwords_match(self, *, new_password: str, old_password: str) -> bool: |  | ||||||
|         try: |  | ||||||
|             hasher = identify_hasher(old_password) |  | ||||||
|         except ValueError: |  | ||||||
|             LOGGER.warning( |  | ||||||
|                 "Skipping password; could not load hash algorithm", |  | ||||||
|             ) |  | ||||||
|             return False |  | ||||||
|  |  | ||||||
|         return hasher.verify(new_password, old_password) |  | ||||||
|  |  | ||||||
|     @classmethod |  | ||||||
|     def is_in_use(cls): |  | ||||||
|         """Check if any UniquePasswordPolicy is in use, either through policy bindings |  | ||||||
|         or direct attachment to a PromptStage. |  | ||||||
|  |  | ||||||
|         Returns: |  | ||||||
|             bool: True if any policy is in use, False otherwise |  | ||||||
|         """ |  | ||||||
|         from authentik.policies.models import PolicyBinding |  | ||||||
|  |  | ||||||
|         # Check if any policy is in use through bindings |  | ||||||
|         if PolicyBinding.in_use.for_policy(cls).exists(): |  | ||||||
|             return True |  | ||||||
|  |  | ||||||
|         # Check if any policy is attached to a PromptStage |  | ||||||
|         if cls.objects.filter(promptstage__isnull=False).exists(): |  | ||||||
|             return True |  | ||||||
|  |  | ||||||
|         return False |  | ||||||
|  |  | ||||||
|     class Meta(Policy.PolicyMeta): |  | ||||||
|         verbose_name = _("Password Uniqueness Policy") |  | ||||||
|         verbose_name_plural = _("Password Uniqueness Policies") |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class UserPasswordHistory(models.Model): |  | ||||||
|     user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="old_passwords") |  | ||||||
|     # Mimic's column type of AbstractBaseUser.password |  | ||||||
|     old_password = models.CharField(max_length=128) |  | ||||||
|     created_at = models.DateTimeField(auto_now_add=True) |  | ||||||
|  |  | ||||||
|     hibp_prefix_sha1 = models.CharField(max_length=5) |  | ||||||
|     hibp_pw_hash = models.TextField() |  | ||||||
|  |  | ||||||
|     class Meta: |  | ||||||
|         verbose_name = _("User Password History") |  | ||||||
|  |  | ||||||
|     def __str__(self) -> str: |  | ||||||
|         timestamp = f"{self.created_at:%Y/%m/%d %X}" if self.created_at else "N/A" |  | ||||||
|         return f"Previous Password (user: {self.user_id}, recorded: {timestamp})" |  | ||||||
|  |  | ||||||
|     @classmethod |  | ||||||
|     def create_for_user(cls, user: User, password: str): |  | ||||||
|         # To check users' passwords against Have I been Pwned, we need the first 5 chars |  | ||||||
|         # of the password hashed with SHA1 without a salt... |  | ||||||
|         pw_hash_sha1 = sha1(password.encode("utf-8")).hexdigest()  # nosec |  | ||||||
|         # ...however that'll give us a list of hashes from HIBP, and to compare that we still |  | ||||||
|         # need a full unsalted SHA1 of the password. We don't want to save that directly in |  | ||||||
|         # the database, so we hash that SHA1 again with a modern hashing alg, |  | ||||||
|         # and then when we check users' passwords against HIBP we can use `check_password` |  | ||||||
|         # which will take care of this. |  | ||||||
|         hibp_hash_hash = make_password(pw_hash_sha1) |  | ||||||
|         return cls.objects.create( |  | ||||||
|             user=user, |  | ||||||
|             old_password=password, |  | ||||||
|             hibp_prefix_sha1=pw_hash_sha1[:5], |  | ||||||
|             hibp_pw_hash=hibp_hash_hash, |  | ||||||
|         ) |  | ||||||
| @ -1,20 +0,0 @@ | |||||||
| """Unique Password Policy settings""" |  | ||||||
|  |  | ||||||
| from celery.schedules import crontab |  | ||||||
|  |  | ||||||
| from authentik.lib.utils.time import fqdn_rand |  | ||||||
|  |  | ||||||
| CELERY_BEAT_SCHEDULE = { |  | ||||||
|     "policies_unique_password_trim_history": { |  | ||||||
|         "task": "authentik.enterprise.policies.unique_password.tasks.trim_password_histories", |  | ||||||
|         "schedule": crontab(minute=fqdn_rand("policies_unique_password_trim"), hour="*/12"), |  | ||||||
|         "options": {"queue": "authentik_scheduled"}, |  | ||||||
|     }, |  | ||||||
|     "policies_unique_password_check_purge": { |  | ||||||
|         "task": ( |  | ||||||
|             "authentik.enterprise.policies.unique_password.tasks.check_and_purge_password_history" |  | ||||||
|         ), |  | ||||||
|         "schedule": crontab(minute=fqdn_rand("policies_unique_password_purge"), hour="*/24"), |  | ||||||
|         "options": {"queue": "authentik_scheduled"}, |  | ||||||
|     }, |  | ||||||
| } |  | ||||||
| @ -1,23 +0,0 @@ | |||||||
| """authentik policy signals""" |  | ||||||
|  |  | ||||||
| from django.dispatch import receiver |  | ||||||
|  |  | ||||||
| from authentik.core.models import User |  | ||||||
| from authentik.core.signals import password_changed |  | ||||||
| from authentik.enterprise.policies.unique_password.models import ( |  | ||||||
|     UniquePasswordPolicy, |  | ||||||
|     UserPasswordHistory, |  | ||||||
| ) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @receiver(password_changed) |  | ||||||
| def copy_password_to_password_history(sender, user: User, *args, **kwargs): |  | ||||||
|     """Preserve the user's old password if UniquePasswordPolicy is enabled anywhere""" |  | ||||||
|     # Check if any UniquePasswordPolicy is in use |  | ||||||
|     unique_pwd_policy_in_use = UniquePasswordPolicy.is_in_use() |  | ||||||
|  |  | ||||||
|     if unique_pwd_policy_in_use: |  | ||||||
|         """NOTE: Because we run this in a signal after saving the user, |  | ||||||
|         we are not atomically guaranteed to save password history. |  | ||||||
|         """ |  | ||||||
|         UserPasswordHistory.create_for_user(user, user.password) |  | ||||||
| @ -1,66 +0,0 @@ | |||||||
| from django.db.models.aggregates import Count |  | ||||||
| from structlog import get_logger |  | ||||||
|  |  | ||||||
| from authentik.enterprise.policies.unique_password.models import ( |  | ||||||
|     UniquePasswordPolicy, |  | ||||||
|     UserPasswordHistory, |  | ||||||
| ) |  | ||||||
| from authentik.events.system_tasks import SystemTask, TaskStatus, prefill_task |  | ||||||
| from authentik.root.celery import CELERY_APP |  | ||||||
|  |  | ||||||
| LOGGER = get_logger() |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @CELERY_APP.task(bind=True, base=SystemTask) |  | ||||||
| @prefill_task |  | ||||||
| def check_and_purge_password_history(self: SystemTask): |  | ||||||
|     """Check if any UniquePasswordPolicy exists, and if not, purge the password history table. |  | ||||||
|     This is run on a schedule instead of being triggered by policy binding deletion. |  | ||||||
|     """ |  | ||||||
|     if not UniquePasswordPolicy.objects.exists(): |  | ||||||
|         UserPasswordHistory.objects.all().delete() |  | ||||||
|         LOGGER.debug("Purged UserPasswordHistory table as no policies are in use") |  | ||||||
|         self.set_status(TaskStatus.SUCCESSFUL, "Successfully purged UserPasswordHistory") |  | ||||||
|         return |  | ||||||
|  |  | ||||||
|     self.set_status( |  | ||||||
|         TaskStatus.SUCCESSFUL, "Not purging password histories, a unique password policy exists" |  | ||||||
|     ) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @CELERY_APP.task(bind=True, base=SystemTask) |  | ||||||
| def trim_password_histories(self: SystemTask): |  | ||||||
|     """Removes rows from UserPasswordHistory older than |  | ||||||
|     the `n` most recent entries. |  | ||||||
|  |  | ||||||
|     The `n` is defined by the largest configured value for all bound |  | ||||||
|     UniquePasswordPolicy policies. |  | ||||||
|     """ |  | ||||||
|  |  | ||||||
|     # No policy, we'll let the cleanup above do its thing |  | ||||||
|     if not UniquePasswordPolicy.objects.exists(): |  | ||||||
|         return |  | ||||||
|  |  | ||||||
|     num_rows_to_preserve = 0 |  | ||||||
|     for policy in UniquePasswordPolicy.objects.all(): |  | ||||||
|         num_rows_to_preserve = max(num_rows_to_preserve, policy.num_historical_passwords) |  | ||||||
|  |  | ||||||
|     all_pks_to_keep = [] |  | ||||||
|  |  | ||||||
|     # Get all users who have password history entries |  | ||||||
|     users_with_history = ( |  | ||||||
|         UserPasswordHistory.objects.values("user") |  | ||||||
|         .annotate(count=Count("user")) |  | ||||||
|         .filter(count__gt=0) |  | ||||||
|         .values_list("user", flat=True) |  | ||||||
|     ) |  | ||||||
|     for user_pk in users_with_history: |  | ||||||
|         entries = UserPasswordHistory.objects.filter(user__pk=user_pk) |  | ||||||
|         pks_to_keep = entries.order_by("-created_at")[:num_rows_to_preserve].values_list( |  | ||||||
|             "pk", flat=True |  | ||||||
|         ) |  | ||||||
|         all_pks_to_keep.extend(pks_to_keep) |  | ||||||
|  |  | ||||||
|     num_deleted, _ = UserPasswordHistory.objects.exclude(pk__in=all_pks_to_keep).delete() |  | ||||||
|     LOGGER.debug("Deleted stale password history records", count=num_deleted) |  | ||||||
|     self.set_status(TaskStatus.SUCCESSFUL, f"Delete {num_deleted} stale password history records") |  | ||||||
| @ -1,108 +0,0 @@ | |||||||
| """Unique Password Policy flow tests""" |  | ||||||
|  |  | ||||||
| from django.contrib.auth.hashers import make_password |  | ||||||
| from django.urls.base import reverse |  | ||||||
|  |  | ||||||
| from authentik.core.tests.utils import create_test_flow, create_test_user |  | ||||||
| from authentik.enterprise.policies.unique_password.models import ( |  | ||||||
|     UniquePasswordPolicy, |  | ||||||
|     UserPasswordHistory, |  | ||||||
| ) |  | ||||||
| from authentik.flows.models import FlowDesignation, FlowStageBinding |  | ||||||
| from authentik.flows.tests import FlowTestCase |  | ||||||
| from authentik.lib.generators import generate_id |  | ||||||
| from authentik.stages.prompt.models import FieldTypes, Prompt, PromptStage |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestUniquePasswordPolicyFlow(FlowTestCase): |  | ||||||
|     """Test Unique Password Policy in a flow""" |  | ||||||
|  |  | ||||||
|     REUSED_PASSWORD = "hunter1"  # nosec B105 |  | ||||||
|  |  | ||||||
|     def setUp(self) -> None: |  | ||||||
|         self.user = create_test_user() |  | ||||||
|         self.flow = create_test_flow(FlowDesignation.AUTHENTICATION) |  | ||||||
|  |  | ||||||
|         password_prompt = Prompt.objects.create( |  | ||||||
|             name=generate_id(), |  | ||||||
|             field_key="password", |  | ||||||
|             label="PASSWORD_LABEL", |  | ||||||
|             type=FieldTypes.PASSWORD, |  | ||||||
|             required=True, |  | ||||||
|             placeholder="PASSWORD_PLACEHOLDER", |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|         self.policy = UniquePasswordPolicy.objects.create( |  | ||||||
|             name="password_must_unique", |  | ||||||
|             password_field=password_prompt.field_key, |  | ||||||
|             num_historical_passwords=1, |  | ||||||
|         ) |  | ||||||
|         stage = PromptStage.objects.create(name="prompt-stage") |  | ||||||
|         stage.validation_policies.set([self.policy]) |  | ||||||
|         stage.fields.set( |  | ||||||
|             [ |  | ||||||
|                 password_prompt, |  | ||||||
|             ] |  | ||||||
|         ) |  | ||||||
|         FlowStageBinding.objects.create(target=self.flow, stage=stage, order=2) |  | ||||||
|  |  | ||||||
|         # Seed the user's password history |  | ||||||
|         UserPasswordHistory.create_for_user(self.user, make_password(self.REUSED_PASSWORD)) |  | ||||||
|  |  | ||||||
|     def test_prompt_data(self): |  | ||||||
|         """Test policy attached to a prompt stage""" |  | ||||||
|         # Test the policy directly |  | ||||||
|         from authentik.policies.types import PolicyRequest |  | ||||||
|         from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT |  | ||||||
|  |  | ||||||
|         # Create a policy request with the reused password |  | ||||||
|         request = PolicyRequest(user=self.user) |  | ||||||
|         request.context[PLAN_CONTEXT_PROMPT] = {"password": self.REUSED_PASSWORD} |  | ||||||
|  |  | ||||||
|         # Test the policy directly |  | ||||||
|         result = self.policy.passes(request) |  | ||||||
|  |  | ||||||
|         # Verify that the policy fails (returns False) with the expected error message |  | ||||||
|         self.assertFalse(result.passing, "Policy should fail for reused password") |  | ||||||
|         self.assertEqual( |  | ||||||
|             result.messages[0], |  | ||||||
|             "This password has been used previously. Please choose a different one.", |  | ||||||
|             "Incorrect error message", |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|         # API-based testing approach: |  | ||||||
|  |  | ||||||
|         self.client.force_login(self.user) |  | ||||||
|  |  | ||||||
|         # Send a POST request to the flow executor with the reused password |  | ||||||
|         response = self.client.post( |  | ||||||
|             reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}), |  | ||||||
|             {"password": self.REUSED_PASSWORD}, |  | ||||||
|         ) |  | ||||||
|         self.assertStageResponse( |  | ||||||
|             response, |  | ||||||
|             self.flow, |  | ||||||
|             component="ak-stage-prompt", |  | ||||||
|             fields=[ |  | ||||||
|                 { |  | ||||||
|                     "choices": None, |  | ||||||
|                     "field_key": "password", |  | ||||||
|                     "label": "PASSWORD_LABEL", |  | ||||||
|                     "order": 0, |  | ||||||
|                     "placeholder": "PASSWORD_PLACEHOLDER", |  | ||||||
|                     "initial_value": "", |  | ||||||
|                     "required": True, |  | ||||||
|                     "type": "password", |  | ||||||
|                     "sub_text": "", |  | ||||||
|                 } |  | ||||||
|             ], |  | ||||||
|             response_errors={ |  | ||||||
|                 "non_field_errors": [ |  | ||||||
|                     { |  | ||||||
|                         "code": "invalid", |  | ||||||
|                         "string": "This password has been used previously. " |  | ||||||
|                         "Please choose a different one.", |  | ||||||
|                     } |  | ||||||
|                 ] |  | ||||||
|             }, |  | ||||||
|         ) |  | ||||||
| @ -1,77 +0,0 @@ | |||||||
| """Unique Password Policy tests""" |  | ||||||
|  |  | ||||||
| from django.contrib.auth.hashers import make_password |  | ||||||
| from django.test import TestCase |  | ||||||
| from guardian.shortcuts import get_anonymous_user |  | ||||||
|  |  | ||||||
| from authentik.core.models import User |  | ||||||
| from authentik.enterprise.policies.unique_password.models import ( |  | ||||||
|     UniquePasswordPolicy, |  | ||||||
|     UserPasswordHistory, |  | ||||||
| ) |  | ||||||
| from authentik.policies.types import PolicyRequest, PolicyResult |  | ||||||
| from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestUniquePasswordPolicy(TestCase): |  | ||||||
|     """Test Password Uniqueness Policy""" |  | ||||||
|  |  | ||||||
|     def setUp(self) -> None: |  | ||||||
|         self.policy = UniquePasswordPolicy.objects.create( |  | ||||||
|             name="test_unique_password", num_historical_passwords=1 |  | ||||||
|         ) |  | ||||||
|         self.user = User.objects.create(username="test-user") |  | ||||||
|  |  | ||||||
|     def test_invalid(self): |  | ||||||
|         """Test without password present in request""" |  | ||||||
|         request = PolicyRequest(get_anonymous_user()) |  | ||||||
|         result: PolicyResult = self.policy.passes(request) |  | ||||||
|         self.assertFalse(result.passing) |  | ||||||
|         self.assertEqual(result.messages[0], "Password not set in context") |  | ||||||
|  |  | ||||||
|     def test_passes_no_previous_passwords(self): |  | ||||||
|         request = PolicyRequest(get_anonymous_user()) |  | ||||||
|         request.context = {PLAN_CONTEXT_PROMPT: {"password": "hunter2"}} |  | ||||||
|         result: PolicyResult = self.policy.passes(request) |  | ||||||
|         self.assertTrue(result.passing) |  | ||||||
|  |  | ||||||
|     def test_passes_passwords_are_different(self): |  | ||||||
|         # Seed database with an old password |  | ||||||
|         UserPasswordHistory.create_for_user(self.user, make_password("hunter1")) |  | ||||||
|  |  | ||||||
|         request = PolicyRequest(self.user) |  | ||||||
|         request.context = {PLAN_CONTEXT_PROMPT: {"password": "hunter2"}} |  | ||||||
|         result: PolicyResult = self.policy.passes(request) |  | ||||||
|         self.assertTrue(result.passing) |  | ||||||
|  |  | ||||||
|     def test_passes_multiple_old_passwords(self): |  | ||||||
|         # Seed with multiple old passwords |  | ||||||
|         UserPasswordHistory.objects.bulk_create( |  | ||||||
|             [ |  | ||||||
|                 UserPasswordHistory(user=self.user, old_password=make_password("hunter1")), |  | ||||||
|                 UserPasswordHistory(user=self.user, old_password=make_password("hunter2")), |  | ||||||
|             ] |  | ||||||
|         ) |  | ||||||
|         request = PolicyRequest(self.user) |  | ||||||
|         request.context = {PLAN_CONTEXT_PROMPT: {"password": "hunter3"}} |  | ||||||
|         result: PolicyResult = self.policy.passes(request) |  | ||||||
|         self.assertTrue(result.passing) |  | ||||||
|  |  | ||||||
|     def test_fails_password_matches_old_password(self): |  | ||||||
|         # Seed database with an old password |  | ||||||
|  |  | ||||||
|         UserPasswordHistory.create_for_user(self.user, make_password("hunter1")) |  | ||||||
|  |  | ||||||
|         request = PolicyRequest(self.user) |  | ||||||
|         request.context = {PLAN_CONTEXT_PROMPT: {"password": "hunter1"}} |  | ||||||
|         result: PolicyResult = self.policy.passes(request) |  | ||||||
|         self.assertFalse(result.passing) |  | ||||||
|  |  | ||||||
|     def test_fails_if_identical_password_with_different_hash_algos(self): |  | ||||||
|         UserPasswordHistory.create_for_user( |  | ||||||
|             self.user, make_password("hunter2", "somesalt", "scrypt") |  | ||||||
|         ) |  | ||||||
|         request = PolicyRequest(self.user) |  | ||||||
|         request.context = {PLAN_CONTEXT_PROMPT: {"password": "hunter2"}} |  | ||||||
|         result: PolicyResult = self.policy.passes(request) |  | ||||||
|         self.assertFalse(result.passing) |  | ||||||
| @ -1,90 +0,0 @@ | |||||||
| from django.urls import reverse |  | ||||||
|  |  | ||||||
| from authentik.core.models import Group, Source, User |  | ||||||
| from authentik.core.tests.utils import create_test_flow, create_test_user |  | ||||||
| from authentik.enterprise.policies.unique_password.models import ( |  | ||||||
|     UniquePasswordPolicy, |  | ||||||
|     UserPasswordHistory, |  | ||||||
| ) |  | ||||||
| from authentik.flows.markers import StageMarker |  | ||||||
| from authentik.flows.models import FlowStageBinding |  | ||||||
| from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan |  | ||||||
| from authentik.flows.tests import FlowTestCase |  | ||||||
| from authentik.flows.views.executor import SESSION_KEY_PLAN |  | ||||||
| from authentik.lib.generators import generate_key |  | ||||||
| from authentik.policies.models import PolicyBinding, PolicyBindingModel |  | ||||||
| from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT |  | ||||||
| from authentik.stages.user_write.models import UserWriteStage |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestUserWriteStage(FlowTestCase): |  | ||||||
|     """Write tests""" |  | ||||||
|  |  | ||||||
|     def setUp(self): |  | ||||||
|         super().setUp() |  | ||||||
|         self.flow = create_test_flow() |  | ||||||
|         self.group = Group.objects.create(name="test-group") |  | ||||||
|         self.other_group = Group.objects.create(name="other-group") |  | ||||||
|         self.stage: UserWriteStage = UserWriteStage.objects.create( |  | ||||||
|             name="write", create_users_as_inactive=True, create_users_group=self.group |  | ||||||
|         ) |  | ||||||
|         self.binding = FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2) |  | ||||||
|         self.source = Source.objects.create(name="fake_source") |  | ||||||
|  |  | ||||||
|     def test_save_password_history_if_policy_binding_enforced(self): |  | ||||||
|         """Test user's new password is recorded when ANY enabled UniquePasswordPolicy exists""" |  | ||||||
|         unique_password_policy = UniquePasswordPolicy.objects.create(num_historical_passwords=5) |  | ||||||
|         pbm = PolicyBindingModel.objects.create() |  | ||||||
|         PolicyBinding.objects.create( |  | ||||||
|             target=pbm, policy=unique_password_policy, order=0, enabled=True |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|         test_user = create_test_user() |  | ||||||
|         # Store original password for verification |  | ||||||
|         original_password = test_user.password |  | ||||||
|  |  | ||||||
|         # We're changing our own password |  | ||||||
|         self.client.force_login(test_user) |  | ||||||
|  |  | ||||||
|         new_password = generate_key() |  | ||||||
|         plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]) |  | ||||||
|         plan.context[PLAN_CONTEXT_PENDING_USER] = test_user |  | ||||||
|         plan.context[PLAN_CONTEXT_PROMPT] = { |  | ||||||
|             "username": test_user.username, |  | ||||||
|             "password": new_password, |  | ||||||
|         } |  | ||||||
|         session = self.client.session |  | ||||||
|         session[SESSION_KEY_PLAN] = plan |  | ||||||
|         session.save() |  | ||||||
|         # Password history should be recorded |  | ||||||
|         user_password_history_qs = UserPasswordHistory.objects.filter(user=test_user) |  | ||||||
|         self.assertTrue(user_password_history_qs.exists(), "Password history should be recorded") |  | ||||||
|         self.assertEqual(len(user_password_history_qs), 1, "expected 1 recorded password") |  | ||||||
|  |  | ||||||
|         # Create a password history entry manually to simulate the signal behavior |  | ||||||
|         # This is what would happen if the signal worked correctly |  | ||||||
|         UserPasswordHistory.objects.create(user=test_user, old_password=original_password) |  | ||||||
|         user_password_history_qs = UserPasswordHistory.objects.filter(user=test_user) |  | ||||||
|         self.assertTrue(user_password_history_qs.exists(), "Password history should be recorded") |  | ||||||
|         self.assertEqual(len(user_password_history_qs), 2, "expected 2 recorded password") |  | ||||||
|  |  | ||||||
|         # Execute the flow by sending a POST request to the flow executor endpoint |  | ||||||
|         response = self.client.post( |  | ||||||
|             reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}) |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|         # Verify that the request was successful |  | ||||||
|         self.assertEqual(response.status_code, 200) |  | ||||||
|         user_qs = User.objects.filter(username=plan.context[PLAN_CONTEXT_PROMPT]["username"]) |  | ||||||
|         self.assertTrue(user_qs.exists()) |  | ||||||
|  |  | ||||||
|         # Verify the password history entry exists |  | ||||||
|         user_password_history_qs = UserPasswordHistory.objects.filter(user=test_user) |  | ||||||
|         self.assertTrue(user_password_history_qs.exists(), "Password history should be recorded") |  | ||||||
|  |  | ||||||
|         self.assertEqual(len(user_password_history_qs), 3, "expected 3 recorded password") |  | ||||||
|         # Verify that one of the entries contains the original password |  | ||||||
|         self.assertTrue( |  | ||||||
|             any(entry.old_password == original_password for entry in user_password_history_qs), |  | ||||||
|             "original password should be in password history table", |  | ||||||
|         ) |  | ||||||
| @ -1,178 +0,0 @@ | |||||||
| from datetime import datetime, timedelta |  | ||||||
|  |  | ||||||
| from django.test import TestCase |  | ||||||
|  |  | ||||||
| from authentik.core.tests.utils import create_test_user |  | ||||||
| from authentik.enterprise.policies.unique_password.models import ( |  | ||||||
|     UniquePasswordPolicy, |  | ||||||
|     UserPasswordHistory, |  | ||||||
| ) |  | ||||||
| from authentik.enterprise.policies.unique_password.tasks import ( |  | ||||||
|     check_and_purge_password_history, |  | ||||||
|     trim_password_histories, |  | ||||||
| ) |  | ||||||
| from authentik.policies.models import PolicyBinding, PolicyBindingModel |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestUniquePasswordPolicyModel(TestCase): |  | ||||||
|     """Test the UniquePasswordPolicy model methods""" |  | ||||||
|  |  | ||||||
|     def test_is_in_use_with_binding(self): |  | ||||||
|         """Test is_in_use returns True when a policy binding exists""" |  | ||||||
|         # Create a UniquePasswordPolicy and a PolicyBinding for it |  | ||||||
|         policy = UniquePasswordPolicy.objects.create(num_historical_passwords=5) |  | ||||||
|         pbm = PolicyBindingModel.objects.create() |  | ||||||
|         PolicyBinding.objects.create(target=pbm, policy=policy, order=0, enabled=True) |  | ||||||
|  |  | ||||||
|         # Verify is_in_use returns True |  | ||||||
|         self.assertTrue(UniquePasswordPolicy.is_in_use()) |  | ||||||
|  |  | ||||||
|     def test_is_in_use_with_promptstage(self): |  | ||||||
|         """Test is_in_use returns True when attached to a PromptStage""" |  | ||||||
|         from authentik.stages.prompt.models import PromptStage |  | ||||||
|  |  | ||||||
|         # Create a UniquePasswordPolicy and attach it to a PromptStage |  | ||||||
|         policy = UniquePasswordPolicy.objects.create(num_historical_passwords=5) |  | ||||||
|         prompt_stage = PromptStage.objects.create( |  | ||||||
|             name="Test Prompt Stage", |  | ||||||
|         ) |  | ||||||
|         # Use the set() method for many-to-many relationships |  | ||||||
|         prompt_stage.validation_policies.set([policy]) |  | ||||||
|  |  | ||||||
|         # Verify is_in_use returns True |  | ||||||
|         self.assertTrue(UniquePasswordPolicy.is_in_use()) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestTrimAllPasswordHistories(TestCase): |  | ||||||
|     """Test the task that trims password history for all users""" |  | ||||||
|  |  | ||||||
|     def setUp(self): |  | ||||||
|         self.user1 = create_test_user("test-user1") |  | ||||||
|         self.user2 = create_test_user("test-user2") |  | ||||||
|         self.pbm = PolicyBindingModel.objects.create() |  | ||||||
|         # Create a policy with a limit of 1 password |  | ||||||
|         self.policy = UniquePasswordPolicy.objects.create(num_historical_passwords=1) |  | ||||||
|         PolicyBinding.objects.create( |  | ||||||
|             target=self.pbm, |  | ||||||
|             policy=self.policy, |  | ||||||
|             enabled=True, |  | ||||||
|             order=0, |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestCheckAndPurgePasswordHistory(TestCase): |  | ||||||
|     """Test the scheduled task that checks if any policy is in use and purges if not""" |  | ||||||
|  |  | ||||||
|     def setUp(self): |  | ||||||
|         self.user = create_test_user("test-user") |  | ||||||
|         self.pbm = PolicyBindingModel.objects.create() |  | ||||||
|  |  | ||||||
|     def test_purge_when_no_policy_in_use(self): |  | ||||||
|         """Test that the task purges the table when no policy is in use""" |  | ||||||
|         # Create some password history entries |  | ||||||
|         UserPasswordHistory.create_for_user(self.user, "hunter2") |  | ||||||
|  |  | ||||||
|         # Verify we have entries |  | ||||||
|         self.assertTrue(UserPasswordHistory.objects.exists()) |  | ||||||
|  |  | ||||||
|         # Run the task - should purge since no policy is in use |  | ||||||
|         check_and_purge_password_history() |  | ||||||
|  |  | ||||||
|         # Verify the table is empty |  | ||||||
|         self.assertFalse(UserPasswordHistory.objects.exists()) |  | ||||||
|  |  | ||||||
|     def test_no_purge_when_policy_in_use(self): |  | ||||||
|         """Test that the task doesn't purge when a policy is in use""" |  | ||||||
|         # Create a policy and binding |  | ||||||
|         policy = UniquePasswordPolicy.objects.create(num_historical_passwords=5) |  | ||||||
|         PolicyBinding.objects.create( |  | ||||||
|             target=self.pbm, |  | ||||||
|             policy=policy, |  | ||||||
|             enabled=True, |  | ||||||
|             order=0, |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|         # Create some password history entries |  | ||||||
|         UserPasswordHistory.create_for_user(self.user, "hunter2") |  | ||||||
|  |  | ||||||
|         # Verify we have entries |  | ||||||
|         self.assertTrue(UserPasswordHistory.objects.exists()) |  | ||||||
|  |  | ||||||
|         # Run the task - should NOT purge since a policy is in use |  | ||||||
|         check_and_purge_password_history() |  | ||||||
|  |  | ||||||
|         # Verify the entries still exist |  | ||||||
|         self.assertTrue(UserPasswordHistory.objects.exists()) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestTrimPasswordHistory(TestCase): |  | ||||||
|     """Test password history cleanup task""" |  | ||||||
|  |  | ||||||
|     def setUp(self): |  | ||||||
|         self.user = create_test_user("test-user") |  | ||||||
|         self.pbm = PolicyBindingModel.objects.create() |  | ||||||
|  |  | ||||||
|     def test_trim_password_history_ok(self): |  | ||||||
|         """Test passwords over the define limit are deleted""" |  | ||||||
|         _now = datetime.now() |  | ||||||
|         UserPasswordHistory.objects.bulk_create( |  | ||||||
|             [ |  | ||||||
|                 UserPasswordHistory( |  | ||||||
|                     user=self.user, |  | ||||||
|                     old_password="hunter1",  # nosec B106 |  | ||||||
|                     created_at=_now - timedelta(days=3), |  | ||||||
|                 ), |  | ||||||
|                 UserPasswordHistory( |  | ||||||
|                     user=self.user, |  | ||||||
|                     old_password="hunter2",  # nosec B106 |  | ||||||
|                     created_at=_now - timedelta(days=2), |  | ||||||
|                 ), |  | ||||||
|                 UserPasswordHistory( |  | ||||||
|                     user=self.user, |  | ||||||
|                     old_password="hunter3",  # nosec B106 |  | ||||||
|                     created_at=_now, |  | ||||||
|                 ), |  | ||||||
|             ] |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|         policy = UniquePasswordPolicy.objects.create(num_historical_passwords=1) |  | ||||||
|         PolicyBinding.objects.create( |  | ||||||
|             target=self.pbm, |  | ||||||
|             policy=policy, |  | ||||||
|             enabled=True, |  | ||||||
|             order=0, |  | ||||||
|         ) |  | ||||||
|         trim_password_histories.delay() |  | ||||||
|         user_pwd_history_qs = UserPasswordHistory.objects.filter(user=self.user) |  | ||||||
|         self.assertEqual(len(user_pwd_history_qs), 1) |  | ||||||
|  |  | ||||||
|     def test_trim_password_history_policy_diabled_no_op(self): |  | ||||||
|         """Test no passwords removed if policy binding is disabled""" |  | ||||||
|  |  | ||||||
|         # Insert a record to ensure it's not deleted after executing task |  | ||||||
|         UserPasswordHistory.create_for_user(self.user, "hunter2") |  | ||||||
|  |  | ||||||
|         policy = UniquePasswordPolicy.objects.create(num_historical_passwords=1) |  | ||||||
|         PolicyBinding.objects.create( |  | ||||||
|             target=self.pbm, |  | ||||||
|             policy=policy, |  | ||||||
|             enabled=False, |  | ||||||
|             order=0, |  | ||||||
|         ) |  | ||||||
|         trim_password_histories.delay() |  | ||||||
|         self.assertTrue(UserPasswordHistory.objects.filter(user=self.user).exists()) |  | ||||||
|  |  | ||||||
|     def test_trim_password_history_fewer_records_than_maximum_is_no_op(self): |  | ||||||
|         """Test no passwords deleted if fewer passwords exist than limit""" |  | ||||||
|  |  | ||||||
|         UserPasswordHistory.create_for_user(self.user, "hunter2") |  | ||||||
|  |  | ||||||
|         policy = UniquePasswordPolicy.objects.create(num_historical_passwords=2) |  | ||||||
|         PolicyBinding.objects.create( |  | ||||||
|             target=self.pbm, |  | ||||||
|             policy=policy, |  | ||||||
|             enabled=True, |  | ||||||
|             order=0, |  | ||||||
|         ) |  | ||||||
|         trim_password_histories.delay() |  | ||||||
|         self.assertTrue(UserPasswordHistory.objects.filter(user=self.user).exists()) |  | ||||||
| @ -1,7 +0,0 @@ | |||||||
| """API URLs""" |  | ||||||
|  |  | ||||||
| from authentik.enterprise.policies.unique_password.api import UniquePasswordPolicyViewSet |  | ||||||
|  |  | ||||||
| api_urlpatterns = [ |  | ||||||
|     ("policies/unique_password", UniquePasswordPolicyViewSet), |  | ||||||
| ] |  | ||||||
| @ -14,7 +14,6 @@ CELERY_BEAT_SCHEDULE = { | |||||||
|  |  | ||||||
| TENANT_APPS = [ | TENANT_APPS = [ | ||||||
|     "authentik.enterprise.audit", |     "authentik.enterprise.audit", | ||||||
|     "authentik.enterprise.policies.unique_password", |  | ||||||
|     "authentik.enterprise.providers.google_workspace", |     "authentik.enterprise.providers.google_workspace", | ||||||
|     "authentik.enterprise.providers.microsoft_entra", |     "authentik.enterprise.providers.microsoft_entra", | ||||||
|     "authentik.enterprise.providers.ssf", |     "authentik.enterprise.providers.ssf", | ||||||
|  | |||||||
| @ -69,6 +69,7 @@ SESSION_KEY_APPLICATION_PRE = "authentik/flows/application_pre" | |||||||
| SESSION_KEY_GET = "authentik/flows/get" | SESSION_KEY_GET = "authentik/flows/get" | ||||||
| SESSION_KEY_POST = "authentik/flows/post" | SESSION_KEY_POST = "authentik/flows/post" | ||||||
| SESSION_KEY_HISTORY = "authentik/flows/history" | SESSION_KEY_HISTORY = "authentik/flows/history" | ||||||
|  | SESSION_KEY_AUTH_STARTED = "authentik/flows/auth_started" | ||||||
| QS_KEY_TOKEN = "flow_token"  # nosec | QS_KEY_TOKEN = "flow_token"  # nosec | ||||||
| QS_QUERY = "query" | QS_QUERY = "query" | ||||||
|  |  | ||||||
| @ -453,6 +454,7 @@ class FlowExecutorView(APIView): | |||||||
|             SESSION_KEY_APPLICATION_PRE, |             SESSION_KEY_APPLICATION_PRE, | ||||||
|             SESSION_KEY_PLAN, |             SESSION_KEY_PLAN, | ||||||
|             SESSION_KEY_GET, |             SESSION_KEY_GET, | ||||||
|  |             SESSION_KEY_AUTH_STARTED, | ||||||
|             # We might need the initial POST payloads for later requests |             # We might need the initial POST payloads for later requests | ||||||
|             # SESSION_KEY_POST, |             # SESSION_KEY_POST, | ||||||
|             # We don't delete the history on purpose, as a user might |             # We don't delete the history on purpose, as a user might | ||||||
|  | |||||||
| @ -6,14 +6,22 @@ from django.shortcuts import get_object_or_404 | |||||||
| from ua_parser.user_agent_parser import Parse | from ua_parser.user_agent_parser import Parse | ||||||
|  |  | ||||||
| from authentik.core.views.interface import InterfaceView | from authentik.core.views.interface import InterfaceView | ||||||
| from authentik.flows.models import Flow | from authentik.flows.models import Flow, FlowDesignation | ||||||
|  | from authentik.flows.views.executor import SESSION_KEY_AUTH_STARTED | ||||||
|  |  | ||||||
|  |  | ||||||
| class FlowInterfaceView(InterfaceView): | class FlowInterfaceView(InterfaceView): | ||||||
|     """Flow interface""" |     """Flow interface""" | ||||||
|  |  | ||||||
|     def get_context_data(self, **kwargs: Any) -> dict[str, Any]: |     def get_context_data(self, **kwargs: Any) -> dict[str, Any]: | ||||||
|         kwargs["flow"] = get_object_or_404(Flow, slug=self.kwargs.get("flow_slug")) |         flow = get_object_or_404(Flow, slug=self.kwargs.get("flow_slug")) | ||||||
|  |         kwargs["flow"] = flow | ||||||
|  |         if ( | ||||||
|  |             not self.request.user.is_authenticated | ||||||
|  |             and flow.designation == FlowDesignation.AUTHENTICATION | ||||||
|  |         ): | ||||||
|  |             self.request.session[SESSION_KEY_AUTH_STARTED] = True | ||||||
|  |             self.request.session.save() | ||||||
|         kwargs["inspector"] = "inspector" in self.request.GET |         kwargs["inspector"] = "inspector" in self.request.GET | ||||||
|         return super().get_context_data(**kwargs) |         return super().get_context_data(**kwargs) | ||||||
|  |  | ||||||
|  | |||||||
| @ -78,6 +78,7 @@ class PolicyBindingSerializer(ModelSerializer): | |||||||
|             "negate", |             "negate", | ||||||
|             "enabled", |             "enabled", | ||||||
|             "order", |             "order", | ||||||
|  |             "honor_order", | ||||||
|             "timeout", |             "timeout", | ||||||
|             "failure_result", |             "failure_result", | ||||||
|         ] |         ] | ||||||
| @ -110,7 +111,16 @@ class PolicyBindingFilter(FilterSet): | |||||||
|  |  | ||||||
|     class Meta: |     class Meta: | ||||||
|         model = PolicyBinding |         model = PolicyBinding | ||||||
|         fields = ["policy", "policy__isnull", "target", "target_in", "enabled", "order", "timeout"] |         fields = [ | ||||||
|  |             "policy", | ||||||
|  |             "policy__isnull", | ||||||
|  |             "target", | ||||||
|  |             "target_in", | ||||||
|  |             "enabled", | ||||||
|  |             "order", | ||||||
|  |             "honor_order", | ||||||
|  |             "timeout", | ||||||
|  |         ] | ||||||
|  |  | ||||||
|  |  | ||||||
| class PolicyBindingViewSet(UsedByMixin, ModelViewSet): | class PolicyBindingViewSet(UsedByMixin, ModelViewSet): | ||||||
|  | |||||||
| @ -1,8 +1,4 @@ | |||||||
| """Authentik policies app config | """authentik policies app config""" | ||||||
|  |  | ||||||
| Every system policy should be its own Django app under the `policies` app. |  | ||||||
| For example: The 'dummy' policy is available at `authentik.policies.dummy`. |  | ||||||
| """ |  | ||||||
|  |  | ||||||
| from prometheus_client import Gauge, Histogram | from prometheus_client import Gauge, Histogram | ||||||
|  |  | ||||||
| @ -39,3 +35,4 @@ class AuthentikPoliciesConfig(ManagedAppConfig): | |||||||
|     label = "authentik_policies" |     label = "authentik_policies" | ||||||
|     verbose_name = "authentik Policies" |     verbose_name = "authentik Policies" | ||||||
|     default = True |     default = True | ||||||
|  |     mountpoint = "policy/" | ||||||
|  | |||||||
| @ -0,0 +1,40 @@ | |||||||
|  | # Generated by Django 5.1.8 on 2025-04-17 15:13 | ||||||
|  |  | ||||||
|  | from django.conf import settings | ||||||
|  | from django.db import migrations, models | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Migration(migrations.Migration): | ||||||
|  |  | ||||||
|  |     dependencies = [ | ||||||
|  |         ("authentik_core", "0047_delete_oldauthenticatedsession"), | ||||||
|  |         ("authentik_policies", "0011_policybinding_failure_result_and_more"), | ||||||
|  |         migrations.swappable_dependency(settings.AUTH_USER_MODEL), | ||||||
|  |     ] | ||||||
|  |  | ||||||
|  |     operations = [ | ||||||
|  |         migrations.AddConstraint( | ||||||
|  |             model_name="policybinding", | ||||||
|  |             constraint=models.CheckConstraint( | ||||||
|  |                 condition=models.Q( | ||||||
|  |                     models.Q( | ||||||
|  |                         ("policy_id__isnull", False), | ||||||
|  |                         ("group_id__isnull", True), | ||||||
|  |                         ("user_id__isnull", True), | ||||||
|  |                     ), | ||||||
|  |                     models.Q( | ||||||
|  |                         ("group_id__isnull", False), | ||||||
|  |                         ("policy_id__isnull", True), | ||||||
|  |                         ("user_id__isnull", True), | ||||||
|  |                     ), | ||||||
|  |                     models.Q( | ||||||
|  |                         ("user_id__isnull", False), | ||||||
|  |                         ("policy_id__isnull", True), | ||||||
|  |                         ("group_id__isnull", True), | ||||||
|  |                     ), | ||||||
|  |                     _connector="OR", | ||||||
|  |                 ), | ||||||
|  |                 name="authentik_policies_policybinding_only_one_type", | ||||||
|  |             ), | ||||||
|  |         ), | ||||||
|  |     ] | ||||||
| @ -0,0 +1,20 @@ | |||||||
|  | # Generated by Django 5.1.8 on 2025-04-17 15:16 | ||||||
|  |  | ||||||
|  | from django.db import migrations, models | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Migration(migrations.Migration): | ||||||
|  |  | ||||||
|  |     dependencies = [ | ||||||
|  |         ("authentik_policies", "0012_policybinding_authentik_policies_policybinding_only_one_type"), | ||||||
|  |     ] | ||||||
|  |  | ||||||
|  |     operations = [ | ||||||
|  |         migrations.AddField( | ||||||
|  |             model_name="policybinding", | ||||||
|  |             name="honor_order", | ||||||
|  |             field=models.BooleanField( | ||||||
|  |                 default=False, help_text="Honor order when evaluating policies." | ||||||
|  |             ), | ||||||
|  |         ), | ||||||
|  |     ] | ||||||
| @ -3,6 +3,7 @@ | |||||||
| from uuid import uuid4 | from uuid import uuid4 | ||||||
|  |  | ||||||
| from django.db import models | from django.db import models | ||||||
|  | from django.db.models import Q | ||||||
| from django.utils.translation import gettext_lazy as _ | from django.utils.translation import gettext_lazy as _ | ||||||
| from model_utils.managers import InheritanceManager | from model_utils.managers import InheritanceManager | ||||||
| from rest_framework.serializers import BaseSerializer | from rest_framework.serializers import BaseSerializer | ||||||
| @ -52,13 +53,6 @@ class PolicyBindingModel(models.Model): | |||||||
|         return ["policy", "user", "group"] |         return ["policy", "user", "group"] | ||||||
|  |  | ||||||
|  |  | ||||||
| class BoundPolicyQuerySet(models.QuerySet): |  | ||||||
|     """QuerySet for filtering enabled bindings for a Policy type""" |  | ||||||
|  |  | ||||||
|     def for_policy(self, policy: "Policy"): |  | ||||||
|         return self.filter(policy__in=policy._default_manager.all()).filter(enabled=True) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class PolicyBinding(SerializerModel): | class PolicyBinding(SerializerModel): | ||||||
|     """Relationship between a Policy and a PolicyBindingModel.""" |     """Relationship between a Policy and a PolicyBindingModel.""" | ||||||
|  |  | ||||||
| @ -107,6 +101,10 @@ class PolicyBinding(SerializerModel): | |||||||
|     ) |     ) | ||||||
|  |  | ||||||
|     order = models.IntegerField() |     order = models.IntegerField() | ||||||
|  |     honor_order = models.BooleanField( | ||||||
|  |         default=False, | ||||||
|  |         help_text=_("Honor order when evaluating policies."), | ||||||
|  |     ) | ||||||
|  |  | ||||||
|     def passes(self, request: PolicyRequest) -> PolicyResult: |     def passes(self, request: PolicyRequest) -> PolicyResult: | ||||||
|         """Check if request passes this PolicyBinding, check policy, group or user""" |         """Check if request passes this PolicyBinding, check policy, group or user""" | ||||||
| @ -155,9 +153,6 @@ class PolicyBinding(SerializerModel): | |||||||
|             return f"Binding - #{self.order} to {suffix}" |             return f"Binding - #{self.order} to {suffix}" | ||||||
|         return "" |         return "" | ||||||
|  |  | ||||||
|     objects = models.Manager() |  | ||||||
|     in_use = BoundPolicyQuerySet.as_manager() |  | ||||||
|  |  | ||||||
|     class Meta: |     class Meta: | ||||||
|         verbose_name = _("Policy Binding") |         verbose_name = _("Policy Binding") | ||||||
|         verbose_name_plural = _("Policy Bindings") |         verbose_name_plural = _("Policy Bindings") | ||||||
| @ -168,6 +163,28 @@ class PolicyBinding(SerializerModel): | |||||||
|             models.Index(fields=["user"]), |             models.Index(fields=["user"]), | ||||||
|             models.Index(fields=["target"]), |             models.Index(fields=["target"]), | ||||||
|         ] |         ] | ||||||
|  |         constraints = ( | ||||||
|  |             models.CheckConstraint( | ||||||
|  |                 condition=( | ||||||
|  |                     ( | ||||||
|  |                         Q(policy_id__isnull=False) | ||||||
|  |                         & Q(group_id__isnull=True) | ||||||
|  |                         & Q(user_id__isnull=True) | ||||||
|  |                     ) | ||||||
|  |                     | ( | ||||||
|  |                         Q(group_id__isnull=False) | ||||||
|  |                         & Q(policy_id__isnull=True) | ||||||
|  |                         & Q(user_id__isnull=True) | ||||||
|  |                     ) | ||||||
|  |                     | ( | ||||||
|  |                         Q(user_id__isnull=False) | ||||||
|  |                         & Q(policy_id__isnull=True) | ||||||
|  |                         & Q(group_id__isnull=True) | ||||||
|  |                     ) | ||||||
|  |                 ), | ||||||
|  |                 name="%(app_label)s_%(class)s_only_one_type", | ||||||
|  |             ), | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |  | ||||||
| class Policy(SerializerModel, CreatedUpdatedModel): | class Policy(SerializerModel, CreatedUpdatedModel): | ||||||
|  | |||||||
| @ -2,6 +2,4 @@ | |||||||
|  |  | ||||||
| from authentik.policies.password.api import PasswordPolicyViewSet | from authentik.policies.password.api import PasswordPolicyViewSet | ||||||
|  |  | ||||||
| api_urlpatterns = [ | api_urlpatterns = [("policies/password", PasswordPolicyViewSet)] | ||||||
|     ("policies/password", PasswordPolicyViewSet), |  | ||||||
| ] |  | ||||||
|  | |||||||
							
								
								
									
										89
									
								
								authentik/policies/templates/policies/buffer.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								authentik/policies/templates/policies/buffer.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,89 @@ | |||||||
|  | {% extends 'login/base_full.html' %} | ||||||
|  |  | ||||||
|  | {% load static %} | ||||||
|  | {% load i18n %} | ||||||
|  |  | ||||||
|  | {% block head %} | ||||||
|  | {{ block.super }} | ||||||
|  | <script> | ||||||
|  |   let redirecting = false; | ||||||
|  |   const checkAuth = async () => { | ||||||
|  |     if (redirecting) return true; | ||||||
|  |     const url = "{{ check_auth_url }}"; | ||||||
|  |     console.debug("authentik/policies/buffer: Checking authentication..."); | ||||||
|  |     try { | ||||||
|  |       const result = await fetch(url, { | ||||||
|  |         method: "HEAD", | ||||||
|  |       }); | ||||||
|  |       if (result.status >= 400) { | ||||||
|  |         return false | ||||||
|  |       } | ||||||
|  |       console.debug("authentik/policies/buffer: Continuing"); | ||||||
|  |       redirecting = true; | ||||||
|  |       if ("{{ auth_req_method }}" === "post") { | ||||||
|  |         document.querySelector("form").submit(); | ||||||
|  |       } else { | ||||||
|  |         window.location.assign("{{ continue_url|escapejs }}"); | ||||||
|  |       } | ||||||
|  |     } catch { | ||||||
|  |       return false; | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  |   let timeout = 100; | ||||||
|  |   let offset = 20; | ||||||
|  |   let attempt = 0; | ||||||
|  |   const main = async () => { | ||||||
|  |     attempt += 1; | ||||||
|  |     await checkAuth(); | ||||||
|  |     console.debug(`authentik/policies/buffer: Waiting ${timeout}ms...`); | ||||||
|  |     setTimeout(main, timeout); | ||||||
|  |     timeout += (offset * attempt); | ||||||
|  |     if (timeout >= 2000) { | ||||||
|  |       timeout = 2000; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   document.addEventListener("visibilitychange", async () => { | ||||||
|  |     if (document.hidden) return; | ||||||
|  |     console.debug("authentik/policies/buffer: Checking authentication on tab activate..."); | ||||||
|  |     await checkAuth(); | ||||||
|  |   }); | ||||||
|  |   main(); | ||||||
|  | </script> | ||||||
|  | {% endblock %} | ||||||
|  |  | ||||||
|  | {% block title %} | ||||||
|  | {% trans 'Waiting for authentication...' %} - {{ brand.branding_title }} | ||||||
|  | {% endblock %} | ||||||
|  |  | ||||||
|  | {% block card_title %} | ||||||
|  | {% trans 'Waiting for authentication...' %} | ||||||
|  | {% endblock %} | ||||||
|  |  | ||||||
|  | {% block card %} | ||||||
|  | <form class="pf-c-form" method="{{ auth_req_method }}" action="{{ continue_url }}"> | ||||||
|  |   {% if auth_req_method == "post" %} | ||||||
|  |     {% for key, value in auth_req_body.items %} | ||||||
|  |       <input type="hidden" name="{{ key }}" value="{{ value }}" /> | ||||||
|  |     {% endfor %} | ||||||
|  |   {% endif %} | ||||||
|  |   <div class="pf-c-empty-state"> | ||||||
|  |     <div class="pf-c-empty-state__content"> | ||||||
|  |       <div class="pf-c-empty-state__icon"> | ||||||
|  |         <span class="pf-c-spinner pf-m-xl" role="progressbar"> | ||||||
|  |           <span class="pf-c-spinner__clipper"></span> | ||||||
|  |           <span class="pf-c-spinner__lead-ball"></span> | ||||||
|  |           <span class="pf-c-spinner__tail-ball"></span> | ||||||
|  |         </span> | ||||||
|  |       </div> | ||||||
|  |       <h1 class="pf-c-title pf-m-lg"> | ||||||
|  |         {% trans "You're already authenticating in another tab. This page will refresh once authentication is completed." %} | ||||||
|  |       </h1> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  |   <div class="pf-c-form__group pf-m-action"> | ||||||
|  |     <a href="{{ auth_req_url }}" class="pf-c-button pf-m-primary pf-m-block"> | ||||||
|  |       {% trans "Authenticate in this tab" %} | ||||||
|  |     </a> | ||||||
|  |   </div> | ||||||
|  | </form> | ||||||
|  | {% endblock %} | ||||||
							
								
								
									
										121
									
								
								authentik/policies/tests/test_views.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										121
									
								
								authentik/policies/tests/test_views.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,121 @@ | |||||||
|  | from django.contrib.auth.models import AnonymousUser | ||||||
|  | from django.contrib.sessions.middleware import SessionMiddleware | ||||||
|  | from django.http import HttpResponse | ||||||
|  | from django.test import RequestFactory, TestCase | ||||||
|  | from django.urls import reverse | ||||||
|  |  | ||||||
|  | from authentik.core.models import Application, Provider | ||||||
|  | from authentik.core.tests.utils import create_test_flow, create_test_user | ||||||
|  | from authentik.flows.models import FlowDesignation | ||||||
|  | from authentik.flows.planner import FlowPlan | ||||||
|  | from authentik.flows.views.executor import SESSION_KEY_PLAN | ||||||
|  | from authentik.lib.generators import generate_id | ||||||
|  | from authentik.lib.tests.utils import dummy_get_response | ||||||
|  | from authentik.policies.views import ( | ||||||
|  |     QS_BUFFER_ID, | ||||||
|  |     SESSION_KEY_BUFFER, | ||||||
|  |     BufferedPolicyAccessView, | ||||||
|  |     BufferView, | ||||||
|  |     PolicyAccessView, | ||||||
|  | ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class TestPolicyViews(TestCase): | ||||||
|  |     """Test PolicyAccessView""" | ||||||
|  |  | ||||||
|  |     def setUp(self): | ||||||
|  |         super().setUp() | ||||||
|  |         self.factory = RequestFactory() | ||||||
|  |         self.user = create_test_user() | ||||||
|  |  | ||||||
|  |     def test_pav(self): | ||||||
|  |         """Test simple policy access view""" | ||||||
|  |         provider = Provider.objects.create( | ||||||
|  |             name=generate_id(), | ||||||
|  |         ) | ||||||
|  |         app = Application.objects.create(name=generate_id(), slug=generate_id(), provider=provider) | ||||||
|  |  | ||||||
|  |         class TestView(PolicyAccessView): | ||||||
|  |             def resolve_provider_application(self): | ||||||
|  |                 self.provider = provider | ||||||
|  |                 self.application = app | ||||||
|  |  | ||||||
|  |             def get(self, *args, **kwargs): | ||||||
|  |                 return HttpResponse("foo") | ||||||
|  |  | ||||||
|  |         req = self.factory.get("/") | ||||||
|  |         req.user = self.user | ||||||
|  |         res = TestView.as_view()(req) | ||||||
|  |         self.assertEqual(res.status_code, 200) | ||||||
|  |         self.assertEqual(res.content, b"foo") | ||||||
|  |  | ||||||
|  |     def test_pav_buffer(self): | ||||||
|  |         """Test simple policy access view""" | ||||||
|  |         provider = Provider.objects.create( | ||||||
|  |             name=generate_id(), | ||||||
|  |         ) | ||||||
|  |         app = Application.objects.create(name=generate_id(), slug=generate_id(), provider=provider) | ||||||
|  |         flow = create_test_flow(FlowDesignation.AUTHENTICATION) | ||||||
|  |  | ||||||
|  |         class TestView(BufferedPolicyAccessView): | ||||||
|  |             def resolve_provider_application(self): | ||||||
|  |                 self.provider = provider | ||||||
|  |                 self.application = app | ||||||
|  |  | ||||||
|  |             def get(self, *args, **kwargs): | ||||||
|  |                 return HttpResponse("foo") | ||||||
|  |  | ||||||
|  |         req = self.factory.get("/") | ||||||
|  |         req.user = AnonymousUser() | ||||||
|  |         middleware = SessionMiddleware(dummy_get_response) | ||||||
|  |         middleware.process_request(req) | ||||||
|  |         req.session[SESSION_KEY_PLAN] = FlowPlan(flow.pk) | ||||||
|  |         req.session.save() | ||||||
|  |         res = TestView.as_view()(req) | ||||||
|  |         self.assertEqual(res.status_code, 302) | ||||||
|  |         self.assertTrue(res.url.startswith(reverse("authentik_policies:buffer"))) | ||||||
|  |  | ||||||
|  |     def test_pav_buffer_skip(self): | ||||||
|  |         """Test simple policy access view (skip buffer)""" | ||||||
|  |         provider = Provider.objects.create( | ||||||
|  |             name=generate_id(), | ||||||
|  |         ) | ||||||
|  |         app = Application.objects.create(name=generate_id(), slug=generate_id(), provider=provider) | ||||||
|  |         flow = create_test_flow(FlowDesignation.AUTHENTICATION) | ||||||
|  |  | ||||||
|  |         class TestView(BufferedPolicyAccessView): | ||||||
|  |             def resolve_provider_application(self): | ||||||
|  |                 self.provider = provider | ||||||
|  |                 self.application = app | ||||||
|  |  | ||||||
|  |             def get(self, *args, **kwargs): | ||||||
|  |                 return HttpResponse("foo") | ||||||
|  |  | ||||||
|  |         req = self.factory.get("/?skip_buffer=true") | ||||||
|  |         req.user = AnonymousUser() | ||||||
|  |         middleware = SessionMiddleware(dummy_get_response) | ||||||
|  |         middleware.process_request(req) | ||||||
|  |         req.session[SESSION_KEY_PLAN] = FlowPlan(flow.pk) | ||||||
|  |         req.session.save() | ||||||
|  |         res = TestView.as_view()(req) | ||||||
|  |         self.assertEqual(res.status_code, 302) | ||||||
|  |         self.assertTrue(res.url.startswith(reverse("authentik_flows:default-authentication"))) | ||||||
|  |  | ||||||
|  |     def test_buffer(self): | ||||||
|  |         """Test buffer view""" | ||||||
|  |         uid = generate_id() | ||||||
|  |         req = self.factory.get(f"/?{QS_BUFFER_ID}={uid}") | ||||||
|  |         req.user = AnonymousUser() | ||||||
|  |         middleware = SessionMiddleware(dummy_get_response) | ||||||
|  |         middleware.process_request(req) | ||||||
|  |         ts = generate_id() | ||||||
|  |         req.session[SESSION_KEY_BUFFER % uid] = { | ||||||
|  |             "method": "get", | ||||||
|  |             "body": {}, | ||||||
|  |             "url": f"/{ts}", | ||||||
|  |         } | ||||||
|  |         req.session.save() | ||||||
|  |  | ||||||
|  |         res = BufferView.as_view()(req) | ||||||
|  |         self.assertEqual(res.status_code, 200) | ||||||
|  |         self.assertIn(ts, res.render().content.decode()) | ||||||
| @ -1,7 +1,14 @@ | |||||||
| """API URLs""" | """API URLs""" | ||||||
|  |  | ||||||
|  | from django.urls import path | ||||||
|  |  | ||||||
| from authentik.policies.api.bindings import PolicyBindingViewSet | from authentik.policies.api.bindings import PolicyBindingViewSet | ||||||
| from authentik.policies.api.policies import PolicyViewSet | from authentik.policies.api.policies import PolicyViewSet | ||||||
|  | from authentik.policies.views import BufferView | ||||||
|  |  | ||||||
|  | urlpatterns = [ | ||||||
|  |     path("buffer", BufferView.as_view(), name="buffer"), | ||||||
|  | ] | ||||||
|  |  | ||||||
| api_urlpatterns = [ | api_urlpatterns = [ | ||||||
|     ("policies/all", PolicyViewSet), |     ("policies/all", PolicyViewSet), | ||||||
|  | |||||||
| @ -1,23 +1,37 @@ | |||||||
| """authentik access helper classes""" | """authentik access helper classes""" | ||||||
|  |  | ||||||
| from typing import Any | from typing import Any | ||||||
|  | from uuid import uuid4 | ||||||
|  |  | ||||||
| from django.contrib import messages | from django.contrib import messages | ||||||
| from django.contrib.auth.mixins import AccessMixin | from django.contrib.auth.mixins import AccessMixin | ||||||
| from django.contrib.auth.views import redirect_to_login | from django.contrib.auth.views import redirect_to_login | ||||||
| from django.http import HttpRequest, HttpResponse | from django.http import HttpRequest, HttpResponse, QueryDict | ||||||
|  | from django.shortcuts import redirect | ||||||
|  | from django.urls import reverse | ||||||
|  | from django.utils.http import urlencode | ||||||
| from django.utils.translation import gettext as _ | from django.utils.translation import gettext as _ | ||||||
| from django.views.generic.base import View | from django.views.generic.base import TemplateView, View | ||||||
| from structlog.stdlib import get_logger | from structlog.stdlib import get_logger | ||||||
|  |  | ||||||
| from authentik.core.models import Application, Provider, User | from authentik.core.models import Application, Provider, User | ||||||
| from authentik.flows.views.executor import SESSION_KEY_APPLICATION_PRE, SESSION_KEY_POST | from authentik.flows.models import Flow, FlowDesignation | ||||||
|  | from authentik.flows.planner import FlowPlan | ||||||
|  | from authentik.flows.views.executor import ( | ||||||
|  |     SESSION_KEY_APPLICATION_PRE, | ||||||
|  |     SESSION_KEY_AUTH_STARTED, | ||||||
|  |     SESSION_KEY_PLAN, | ||||||
|  |     SESSION_KEY_POST, | ||||||
|  | ) | ||||||
| from authentik.lib.sentry import SentryIgnoredException | from authentik.lib.sentry import SentryIgnoredException | ||||||
| from authentik.policies.denied import AccessDeniedResponse | from authentik.policies.denied import AccessDeniedResponse | ||||||
| from authentik.policies.engine import PolicyEngine | from authentik.policies.engine import PolicyEngine | ||||||
| from authentik.policies.types import PolicyRequest, PolicyResult | from authentik.policies.types import PolicyRequest, PolicyResult | ||||||
|  |  | ||||||
| LOGGER = get_logger() | LOGGER = get_logger() | ||||||
|  | QS_BUFFER_ID = "af_bf_id" | ||||||
|  | QS_SKIP_BUFFER = "skip_buffer" | ||||||
|  | SESSION_KEY_BUFFER = "authentik/policies/pav_buffer/%s" | ||||||
|  |  | ||||||
|  |  | ||||||
| class RequestValidationError(SentryIgnoredException): | class RequestValidationError(SentryIgnoredException): | ||||||
| @ -125,3 +139,65 @@ class PolicyAccessView(AccessMixin, View): | |||||||
|             for message in result.messages: |             for message in result.messages: | ||||||
|                 messages.error(self.request, _(message)) |                 messages.error(self.request, _(message)) | ||||||
|         return result |         return result | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def url_with_qs(url: str, **kwargs): | ||||||
|  |     """Update/set querystring of `url` with the parameters in `kwargs`. Original query string | ||||||
|  |     parameters are retained""" | ||||||
|  |     if "?" not in url: | ||||||
|  |         return url + f"?{urlencode(kwargs)}" | ||||||
|  |     url, _, qs = url.partition("?") | ||||||
|  |     qs = QueryDict(qs, mutable=True) | ||||||
|  |     qs.update(kwargs) | ||||||
|  |     return url + f"?{urlencode(qs.items())}" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class BufferView(TemplateView): | ||||||
|  |     """Buffer view""" | ||||||
|  |  | ||||||
|  |     template_name = "policies/buffer.html" | ||||||
|  |  | ||||||
|  |     def get_context_data(self, **kwargs): | ||||||
|  |         buf_id = self.request.GET.get(QS_BUFFER_ID) | ||||||
|  |         buffer: dict = self.request.session.get(SESSION_KEY_BUFFER % buf_id) | ||||||
|  |         kwargs["auth_req_method"] = buffer["method"] | ||||||
|  |         kwargs["auth_req_body"] = buffer["body"] | ||||||
|  |         kwargs["auth_req_url"] = url_with_qs(buffer["url"], **{QS_SKIP_BUFFER: True}) | ||||||
|  |         kwargs["check_auth_url"] = reverse("authentik_api:user-me") | ||||||
|  |         kwargs["continue_url"] = url_with_qs(buffer["url"], **{QS_BUFFER_ID: buf_id}) | ||||||
|  |         return super().get_context_data(**kwargs) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class BufferedPolicyAccessView(PolicyAccessView): | ||||||
|  |     """PolicyAccessView which buffers access requests in case the user is not logged in""" | ||||||
|  |  | ||||||
|  |     def handle_no_permission(self): | ||||||
|  |         plan: FlowPlan | None = self.request.session.get(SESSION_KEY_PLAN) | ||||||
|  |         authenticating = self.request.session.get(SESSION_KEY_AUTH_STARTED) | ||||||
|  |         if plan: | ||||||
|  |             flow = Flow.objects.filter(pk=plan.flow_pk).first() | ||||||
|  |             if not flow or flow.designation != FlowDesignation.AUTHENTICATION: | ||||||
|  |                 LOGGER.debug("Not buffering request, no flow or flow not for authentication") | ||||||
|  |                 return super().handle_no_permission() | ||||||
|  |         if not plan and authenticating is None: | ||||||
|  |             LOGGER.debug("Not buffering request, no flow plan active") | ||||||
|  |             return super().handle_no_permission() | ||||||
|  |         if self.request.GET.get(QS_SKIP_BUFFER): | ||||||
|  |             LOGGER.debug("Not buffering request, explicit skip") | ||||||
|  |             return super().handle_no_permission() | ||||||
|  |         buffer_id = str(uuid4()) | ||||||
|  |         LOGGER.debug("Buffering access request", bf_id=buffer_id) | ||||||
|  |         self.request.session[SESSION_KEY_BUFFER % buffer_id] = { | ||||||
|  |             "body": self.request.POST, | ||||||
|  |             "url": self.request.build_absolute_uri(self.request.get_full_path()), | ||||||
|  |             "method": self.request.method.lower(), | ||||||
|  |         } | ||||||
|  |         return redirect( | ||||||
|  |             url_with_qs(reverse("authentik_policies:buffer"), **{QS_BUFFER_ID: buffer_id}) | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |     def dispatch(self, request, *args, **kwargs): | ||||||
|  |         response = super().dispatch(request, *args, **kwargs) | ||||||
|  |         if QS_BUFFER_ID in self.request.GET: | ||||||
|  |             self.request.session.pop(SESSION_KEY_BUFFER % self.request.GET[QS_BUFFER_ID], None) | ||||||
|  |         return response | ||||||
|  | |||||||
| @ -30,7 +30,7 @@ from authentik.flows.stage import StageView | |||||||
| from authentik.lib.utils.time import timedelta_from_string | from authentik.lib.utils.time import timedelta_from_string | ||||||
| from authentik.lib.views import bad_request_message | from authentik.lib.views import bad_request_message | ||||||
| from authentik.policies.types import PolicyRequest | from authentik.policies.types import PolicyRequest | ||||||
| from authentik.policies.views import PolicyAccessView, RequestValidationError | from authentik.policies.views import BufferedPolicyAccessView, RequestValidationError | ||||||
| from authentik.providers.oauth2.constants import ( | from authentik.providers.oauth2.constants import ( | ||||||
|     PKCE_METHOD_PLAIN, |     PKCE_METHOD_PLAIN, | ||||||
|     PKCE_METHOD_S256, |     PKCE_METHOD_S256, | ||||||
| @ -326,7 +326,7 @@ class OAuthAuthorizationParams: | |||||||
|         return code |         return code | ||||||
|  |  | ||||||
|  |  | ||||||
| class AuthorizationFlowInitView(PolicyAccessView): | class AuthorizationFlowInitView(BufferedPolicyAccessView): | ||||||
|     """OAuth2 Flow initializer, checks access to application and starts flow""" |     """OAuth2 Flow initializer, checks access to application and starts flow""" | ||||||
|  |  | ||||||
|     params: OAuthAuthorizationParams |     params: OAuthAuthorizationParams | ||||||
|  | |||||||
| @ -18,11 +18,11 @@ from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, FlowPlanner | |||||||
| from authentik.flows.stage import RedirectStage | from authentik.flows.stage import RedirectStage | ||||||
| from authentik.lib.utils.time import timedelta_from_string | from authentik.lib.utils.time import timedelta_from_string | ||||||
| from authentik.policies.engine import PolicyEngine | from authentik.policies.engine import PolicyEngine | ||||||
| from authentik.policies.views import PolicyAccessView | from authentik.policies.views import BufferedPolicyAccessView | ||||||
| from authentik.providers.rac.models import ConnectionToken, Endpoint, RACProvider | from authentik.providers.rac.models import ConnectionToken, Endpoint, RACProvider | ||||||
|  |  | ||||||
|  |  | ||||||
| class RACStartView(PolicyAccessView): | class RACStartView(BufferedPolicyAccessView): | ||||||
|     """Start a RAC connection by checking access and creating a connection token""" |     """Start a RAC connection by checking access and creating a connection token""" | ||||||
|  |  | ||||||
|     endpoint: Endpoint |     endpoint: Endpoint | ||||||
|  | |||||||
| @ -15,7 +15,7 @@ from authentik.flows.models import in_memory_stage | |||||||
| from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_SSO, FlowPlanner | from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_SSO, FlowPlanner | ||||||
| from authentik.flows.views.executor import SESSION_KEY_POST | from authentik.flows.views.executor import SESSION_KEY_POST | ||||||
| from authentik.lib.views import bad_request_message | from authentik.lib.views import bad_request_message | ||||||
| from authentik.policies.views import PolicyAccessView | from authentik.policies.views import BufferedPolicyAccessView | ||||||
| from authentik.providers.saml.exceptions import CannotHandleAssertion | from authentik.providers.saml.exceptions import CannotHandleAssertion | ||||||
| from authentik.providers.saml.models import SAMLBindings, SAMLProvider | from authentik.providers.saml.models import SAMLBindings, SAMLProvider | ||||||
| from authentik.providers.saml.processors.authn_request_parser import AuthNRequestParser | from authentik.providers.saml.processors.authn_request_parser import AuthNRequestParser | ||||||
| @ -35,7 +35,7 @@ from authentik.stages.consent.stage import ( | |||||||
| LOGGER = get_logger() | LOGGER = get_logger() | ||||||
|  |  | ||||||
|  |  | ||||||
| class SAMLSSOView(PolicyAccessView): | class SAMLSSOView(BufferedPolicyAccessView): | ||||||
|     """SAML SSO Base View, which plans a flow and injects our final stage. |     """SAML SSO Base View, which plans a flow and injects our final stage. | ||||||
|     Calls get/post handler.""" |     Calls get/post handler.""" | ||||||
|  |  | ||||||
| @ -83,7 +83,7 @@ class SAMLSSOView(PolicyAccessView): | |||||||
|  |  | ||||||
|     def post(self, request: HttpRequest, application_slug: str) -> HttpResponse: |     def post(self, request: HttpRequest, application_slug: str) -> HttpResponse: | ||||||
|         """GET and POST use the same handler, but we can't |         """GET and POST use the same handler, but we can't | ||||||
|         override .dispatch easily because PolicyAccessView's dispatch""" |         override .dispatch easily because BufferedPolicyAccessView's dispatch""" | ||||||
|         return self.get(request, application_slug) |         return self.get(request, application_slug) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | |||||||
| @ -171,8 +171,7 @@ def username_field_validator_factory() -> Callable[[PromptChallengeResponse, str | |||||||
|  |  | ||||||
|  |  | ||||||
| def password_single_validator_factory() -> Callable[[PromptChallengeResponse, str], Any]: | def password_single_validator_factory() -> Callable[[PromptChallengeResponse, str], Any]: | ||||||
|     """Return a `clean_` method for `field`. Clean method checks if the password meets configured |     """Return a `clean_` method for `field`. Clean method checks if username is taken already.""" | ||||||
|     PasswordPolicy.""" |  | ||||||
|  |  | ||||||
|     def password_single_clean(self: PromptChallengeResponse, value: str) -> Any: |     def password_single_clean(self: PromptChallengeResponse, value: str) -> Any: | ||||||
|         """Send password validation signals for e.g. LDAP Source""" |         """Send password validation signals for e.g. LDAP Source""" | ||||||
|  | |||||||
| @ -4,13 +4,7 @@ from unittest.mock import patch | |||||||
|  |  | ||||||
| from django.urls import reverse | from django.urls import reverse | ||||||
|  |  | ||||||
| from authentik.core.models import ( | from authentik.core.models import USER_ATTRIBUTE_SOURCES, Group, Source, User, UserSourceConnection | ||||||
|     USER_ATTRIBUTE_SOURCES, |  | ||||||
|     Group, |  | ||||||
|     Source, |  | ||||||
|     User, |  | ||||||
|     UserSourceConnection, |  | ||||||
| ) |  | ||||||
| from authentik.core.sources.stage import PLAN_CONTEXT_SOURCES_CONNECTION | from authentik.core.sources.stage import PLAN_CONTEXT_SOURCES_CONNECTION | ||||||
| from authentik.core.tests.utils import create_test_admin_user, create_test_flow | from authentik.core.tests.utils import create_test_admin_user, create_test_flow | ||||||
| from authentik.events.models import Event, EventAction | from authentik.events.models import Event, EventAction | ||||||
|  | |||||||
| @ -3641,46 +3641,6 @@ | |||||||
|                             } |                             } | ||||||
|                         } |                         } | ||||||
|                     }, |                     }, | ||||||
|                     { |  | ||||||
|                         "type": "object", |  | ||||||
|                         "required": [ |  | ||||||
|                             "model", |  | ||||||
|                             "identifiers" |  | ||||||
|                         ], |  | ||||||
|                         "properties": { |  | ||||||
|                             "model": { |  | ||||||
|                                 "const": "authentik_policies_unique_password.uniquepasswordpolicy" |  | ||||||
|                             }, |  | ||||||
|                             "id": { |  | ||||||
|                                 "type": "string" |  | ||||||
|                             }, |  | ||||||
|                             "state": { |  | ||||||
|                                 "type": "string", |  | ||||||
|                                 "enum": [ |  | ||||||
|                                     "absent", |  | ||||||
|                                     "present", |  | ||||||
|                                     "created", |  | ||||||
|                                     "must_created" |  | ||||||
|                                 ], |  | ||||||
|                                 "default": "present" |  | ||||||
|                             }, |  | ||||||
|                             "conditions": { |  | ||||||
|                                 "type": "array", |  | ||||||
|                                 "items": { |  | ||||||
|                                     "type": "boolean" |  | ||||||
|                                 } |  | ||||||
|                             }, |  | ||||||
|                             "permissions": { |  | ||||||
|                                 "$ref": "#/$defs/model_authentik_policies_unique_password.uniquepasswordpolicy_permissions" |  | ||||||
|                             }, |  | ||||||
|                             "attrs": { |  | ||||||
|                                 "$ref": "#/$defs/model_authentik_policies_unique_password.uniquepasswordpolicy" |  | ||||||
|                             }, |  | ||||||
|                             "identifiers": { |  | ||||||
|                                 "$ref": "#/$defs/model_authentik_policies_unique_password.uniquepasswordpolicy" |  | ||||||
|                             } |  | ||||||
|                         } |  | ||||||
|                     }, |  | ||||||
|                     { |                     { | ||||||
|                         "type": "object", |                         "type": "object", | ||||||
|                         "required": [ |                         "required": [ | ||||||
| @ -4862,7 +4822,6 @@ | |||||||
|                         "authentik.core", |                         "authentik.core", | ||||||
|                         "authentik.enterprise", |                         "authentik.enterprise", | ||||||
|                         "authentik.enterprise.audit", |                         "authentik.enterprise.audit", | ||||||
|                         "authentik.enterprise.policies.unique_password", |  | ||||||
|                         "authentik.enterprise.providers.google_workspace", |                         "authentik.enterprise.providers.google_workspace", | ||||||
|                         "authentik.enterprise.providers.microsoft_entra", |                         "authentik.enterprise.providers.microsoft_entra", | ||||||
|                         "authentik.enterprise.providers.ssf", |                         "authentik.enterprise.providers.ssf", | ||||||
| @ -4970,7 +4929,6 @@ | |||||||
|                         "authentik_core.applicationentitlement", |                         "authentik_core.applicationentitlement", | ||||||
|                         "authentik_core.token", |                         "authentik_core.token", | ||||||
|                         "authentik_enterprise.license", |                         "authentik_enterprise.license", | ||||||
|                         "authentik_policies_unique_password.uniquepasswordpolicy", |  | ||||||
|                         "authentik_providers_google_workspace.googleworkspaceprovider", |                         "authentik_providers_google_workspace.googleworkspaceprovider", | ||||||
|                         "authentik_providers_google_workspace.googleworkspaceprovidermapping", |                         "authentik_providers_google_workspace.googleworkspaceprovidermapping", | ||||||
|                         "authentik_providers_microsoft_entra.microsoftentraprovider", |                         "authentik_providers_microsoft_entra.microsoftentraprovider", | ||||||
| @ -5665,6 +5623,11 @@ | |||||||
|                     "maximum": 2147483647, |                     "maximum": 2147483647, | ||||||
|                     "title": "Order" |                     "title": "Order" | ||||||
|                 }, |                 }, | ||||||
|  |                 "honor_order": { | ||||||
|  |                     "type": "boolean", | ||||||
|  |                     "title": "Honor order", | ||||||
|  |                     "description": "Honor order when evaluating policies." | ||||||
|  |                 }, | ||||||
|                 "timeout": { |                 "timeout": { | ||||||
|                     "type": "integer", |                     "type": "integer", | ||||||
|                     "minimum": 0, |                     "minimum": 0, | ||||||
| @ -7126,14 +7089,6 @@ | |||||||
|                             "authentik_policies_reputation.delete_reputationpolicy", |                             "authentik_policies_reputation.delete_reputationpolicy", | ||||||
|                             "authentik_policies_reputation.view_reputation", |                             "authentik_policies_reputation.view_reputation", | ||||||
|                             "authentik_policies_reputation.view_reputationpolicy", |                             "authentik_policies_reputation.view_reputationpolicy", | ||||||
|                             "authentik_policies_unique_password.add_uniquepasswordpolicy", |  | ||||||
|                             "authentik_policies_unique_password.add_userpasswordhistory", |  | ||||||
|                             "authentik_policies_unique_password.change_uniquepasswordpolicy", |  | ||||||
|                             "authentik_policies_unique_password.change_userpasswordhistory", |  | ||||||
|                             "authentik_policies_unique_password.delete_uniquepasswordpolicy", |  | ||||||
|                             "authentik_policies_unique_password.delete_userpasswordhistory", |  | ||||||
|                             "authentik_policies_unique_password.view_uniquepasswordpolicy", |  | ||||||
|                             "authentik_policies_unique_password.view_userpasswordhistory", |  | ||||||
|                             "authentik_providers_google_workspace.add_googleworkspaceprovider", |                             "authentik_providers_google_workspace.add_googleworkspaceprovider", | ||||||
|                             "authentik_providers_google_workspace.add_googleworkspaceprovidergroup", |                             "authentik_providers_google_workspace.add_googleworkspaceprovidergroup", | ||||||
|                             "authentik_providers_google_workspace.add_googleworkspaceprovidermapping", |                             "authentik_providers_google_workspace.add_googleworkspaceprovidermapping", | ||||||
| @ -13834,14 +13789,6 @@ | |||||||
|                             "authentik_policies_reputation.delete_reputationpolicy", |                             "authentik_policies_reputation.delete_reputationpolicy", | ||||||
|                             "authentik_policies_reputation.view_reputation", |                             "authentik_policies_reputation.view_reputation", | ||||||
|                             "authentik_policies_reputation.view_reputationpolicy", |                             "authentik_policies_reputation.view_reputationpolicy", | ||||||
|                             "authentik_policies_unique_password.add_uniquepasswordpolicy", |  | ||||||
|                             "authentik_policies_unique_password.add_userpasswordhistory", |  | ||||||
|                             "authentik_policies_unique_password.change_uniquepasswordpolicy", |  | ||||||
|                             "authentik_policies_unique_password.change_userpasswordhistory", |  | ||||||
|                             "authentik_policies_unique_password.delete_uniquepasswordpolicy", |  | ||||||
|                             "authentik_policies_unique_password.delete_userpasswordhistory", |  | ||||||
|                             "authentik_policies_unique_password.view_uniquepasswordpolicy", |  | ||||||
|                             "authentik_policies_unique_password.view_userpasswordhistory", |  | ||||||
|                             "authentik_providers_google_workspace.add_googleworkspaceprovider", |                             "authentik_providers_google_workspace.add_googleworkspaceprovider", | ||||||
|                             "authentik_providers_google_workspace.add_googleworkspaceprovidergroup", |                             "authentik_providers_google_workspace.add_googleworkspaceprovidergroup", | ||||||
|                             "authentik_providers_google_workspace.add_googleworkspaceprovidermapping", |                             "authentik_providers_google_workspace.add_googleworkspaceprovidermapping", | ||||||
| @ -14526,61 +14473,6 @@ | |||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         "model_authentik_policies_unique_password.uniquepasswordpolicy": { |  | ||||||
|             "type": "object", |  | ||||||
|             "properties": { |  | ||||||
|                 "name": { |  | ||||||
|                     "type": "string", |  | ||||||
|                     "minLength": 1, |  | ||||||
|                     "title": "Name" |  | ||||||
|                 }, |  | ||||||
|                 "execution_logging": { |  | ||||||
|                     "type": "boolean", |  | ||||||
|                     "title": "Execution logging", |  | ||||||
|                     "description": "When this option is enabled, all executions of this policy will be logged. By default, only execution errors are logged." |  | ||||||
|                 }, |  | ||||||
|                 "password_field": { |  | ||||||
|                     "type": "string", |  | ||||||
|                     "minLength": 1, |  | ||||||
|                     "title": "Password field", |  | ||||||
|                     "description": "Field key to check, field keys defined in Prompt stages are available." |  | ||||||
|                 }, |  | ||||||
|                 "num_historical_passwords": { |  | ||||||
|                     "type": "integer", |  | ||||||
|                     "minimum": 0, |  | ||||||
|                     "maximum": 2147483647, |  | ||||||
|                     "title": "Num historical passwords", |  | ||||||
|                     "description": "Number of passwords to check against." |  | ||||||
|                 } |  | ||||||
|             }, |  | ||||||
|             "required": [] |  | ||||||
|         }, |  | ||||||
|         "model_authentik_policies_unique_password.uniquepasswordpolicy_permissions": { |  | ||||||
|             "type": "array", |  | ||||||
|             "items": { |  | ||||||
|                 "type": "object", |  | ||||||
|                 "required": [ |  | ||||||
|                     "permission" |  | ||||||
|                 ], |  | ||||||
|                 "properties": { |  | ||||||
|                     "permission": { |  | ||||||
|                         "type": "string", |  | ||||||
|                         "enum": [ |  | ||||||
|                             "add_uniquepasswordpolicy", |  | ||||||
|                             "change_uniquepasswordpolicy", |  | ||||||
|                             "delete_uniquepasswordpolicy", |  | ||||||
|                             "view_uniquepasswordpolicy" |  | ||||||
|                         ] |  | ||||||
|                     }, |  | ||||||
|                     "user": { |  | ||||||
|                         "type": "integer" |  | ||||||
|                     }, |  | ||||||
|                     "role": { |  | ||||||
|                         "type": "string" |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         }, |  | ||||||
|         "model_authentik_providers_google_workspace.googleworkspaceprovider": { |         "model_authentik_providers_google_workspace.googleworkspaceprovider": { | ||||||
|             "type": "object", |             "type": "object", | ||||||
|             "properties": { |             "properties": { | ||||||
|  | |||||||
							
								
								
									
										6
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										6
									
								
								go.mod
									
									
									
									
									
								
							| @ -7,7 +7,7 @@ require ( | |||||||
| 	github.com/coreos/go-oidc/v3 v3.14.1 | 	github.com/coreos/go-oidc/v3 v3.14.1 | ||||||
| 	github.com/getsentry/sentry-go v0.32.0 | 	github.com/getsentry/sentry-go v0.32.0 | ||||||
| 	github.com/go-http-utils/etag v0.0.0-20161124023236-513ea8f21eb1 | 	github.com/go-http-utils/etag v0.0.0-20161124023236-513ea8f21eb1 | ||||||
| 	github.com/go-ldap/ldap/v3 v3.4.11 | 	github.com/go-ldap/ldap/v3 v3.4.10 | ||||||
| 	github.com/go-openapi/runtime v0.28.0 | 	github.com/go-openapi/runtime v0.28.0 | ||||||
| 	github.com/golang-jwt/jwt/v5 v5.2.2 | 	github.com/golang-jwt/jwt/v5 v5.2.2 | ||||||
| 	github.com/google/uuid v1.6.0 | 	github.com/google/uuid v1.6.0 | ||||||
| @ -27,7 +27,7 @@ require ( | |||||||
| 	github.com/spf13/cobra v1.9.1 | 	github.com/spf13/cobra v1.9.1 | ||||||
| 	github.com/stretchr/testify v1.10.0 | 	github.com/stretchr/testify v1.10.0 | ||||||
| 	github.com/wwt/guac v1.3.2 | 	github.com/wwt/guac v1.3.2 | ||||||
| 	goauthentik.io/api/v3 v3.2025024.8 | 	goauthentik.io/api/v3 v3.2025024.7 | ||||||
| 	golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab | 	golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab | ||||||
| 	golang.org/x/oauth2 v0.29.0 | 	golang.org/x/oauth2 v0.29.0 | ||||||
| 	golang.org/x/sync v0.13.0 | 	golang.org/x/sync v0.13.0 | ||||||
| @ -43,7 +43,7 @@ require ( | |||||||
| 	github.com/davecgh/go-spew v1.1.1 // indirect | 	github.com/davecgh/go-spew v1.1.1 // indirect | ||||||
| 	github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect | 	github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect | ||||||
| 	github.com/felixge/httpsnoop v1.0.3 // indirect | 	github.com/felixge/httpsnoop v1.0.3 // indirect | ||||||
| 	github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect | 	github.com/go-asn1-ber/asn1-ber v1.5.7 // indirect | ||||||
| 	github.com/go-http-utils/fresh v0.0.0-20161124030543-7231e26a4b27 // indirect | 	github.com/go-http-utils/fresh v0.0.0-20161124030543-7231e26a4b27 // indirect | ||||||
| 	github.com/go-http-utils/headers v0.0.0-20181008091004-fed159eddc2a // indirect | 	github.com/go-http-utils/headers v0.0.0-20181008091004-fed159eddc2a // indirect | ||||||
| 	github.com/go-jose/go-jose/v4 v4.0.5 // indirect | 	github.com/go-jose/go-jose/v4 v4.0.5 // indirect | ||||||
|  | |||||||
							
								
								
									
										82
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										82
									
								
								go.sum
									
									
									
									
									
								
							| @ -71,8 +71,8 @@ github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBd | |||||||
| github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= | github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= | ||||||
| github.com/getsentry/sentry-go v0.32.0 h1:YKs+//QmwE3DcYtfKRH8/KyOOF/I6Qnx7qYGNHCGmCY= | github.com/getsentry/sentry-go v0.32.0 h1:YKs+//QmwE3DcYtfKRH8/KyOOF/I6Qnx7qYGNHCGmCY= | ||||||
| github.com/getsentry/sentry-go v0.32.0/go.mod h1:CYNcMMz73YigoHljQRG+qPF+eMq8gG72XcGN/p71BAY= | github.com/getsentry/sentry-go v0.32.0/go.mod h1:CYNcMMz73YigoHljQRG+qPF+eMq8gG72XcGN/p71BAY= | ||||||
| github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo= | github.com/go-asn1-ber/asn1-ber v1.5.7 h1:DTX+lbVTWaTw1hQ+PbZPlnDZPEIs0SS/GCZAl535dDk= | ||||||
| github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= | github.com/go-asn1-ber/asn1-ber v1.5.7/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= | ||||||
| github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= | github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= | ||||||
| github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= | github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= | ||||||
| github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= | ||||||
| @ -86,8 +86,8 @@ github.com/go-http-utils/headers v0.0.0-20181008091004-fed159eddc2a h1:v6zMvHuY9 | |||||||
| github.com/go-http-utils/headers v0.0.0-20181008091004-fed159eddc2a/go.mod h1:I79BieaU4fxrw4LMXby6q5OS9XnoR9UIKLOzDFjUmuw= | github.com/go-http-utils/headers v0.0.0-20181008091004-fed159eddc2a/go.mod h1:I79BieaU4fxrw4LMXby6q5OS9XnoR9UIKLOzDFjUmuw= | ||||||
| github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= | github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= | ||||||
| github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= | github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= | ||||||
| github.com/go-ldap/ldap/v3 v3.4.11 h1:4k0Yxweg+a3OyBLjdYn5OKglv18JNvfDykSoI8bW0gU= | github.com/go-ldap/ldap/v3 v3.4.10 h1:ot/iwPOhfpNVgB1o+AVXljizWZ9JTp7YF5oeyONmcJU= | ||||||
| github.com/go-ldap/ldap/v3 v3.4.11/go.mod h1:bY7t0FLK8OAVpp/vV6sSlpz3EQDGcQwc8pF0ujLgKvM= | github.com/go-ldap/ldap/v3 v3.4.10/go.mod h1:JXh4Uxgi40P6E9rdsYqpUtbW46D9UTjJ9QSwGRznplY= | ||||||
| github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= | ||||||
| github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= | github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= | ||||||
| github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= | github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= | ||||||
| @ -148,6 +148,7 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ | |||||||
| github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= | github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= | ||||||
| github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= | ||||||
| github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= | github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= | ||||||
|  | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= | ||||||
| github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= | ||||||
| github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= | ||||||
| github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= | github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= | ||||||
| @ -171,13 +172,16 @@ github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyE | |||||||
| github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= | github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= | ||||||
| github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= | github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= | ||||||
| github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= | github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= | ||||||
|  | github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= | ||||||
| github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= | github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= | ||||||
| github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= | github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= | ||||||
|  | github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= | ||||||
| github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ= | github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ= | ||||||
| github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik= | github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik= | ||||||
| github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= | github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= | ||||||
| github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= | github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= | ||||||
| github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= | github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= | ||||||
|  | github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= | ||||||
| github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= | github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= | ||||||
| github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= | github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= | ||||||
| github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= | ||||||
| @ -262,10 +266,15 @@ github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= | |||||||
| github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= | github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= | ||||||
| github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= | ||||||
| github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= | ||||||
|  | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= | ||||||
|  | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= | ||||||
| github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= | ||||||
| github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= | ||||||
| github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= | ||||||
| github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= | ||||||
|  | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= | ||||||
|  | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= | ||||||
|  | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= | ||||||
| github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= | ||||||
| github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= | ||||||
| github.com/wwt/guac v1.3.2 h1:sH6OFGa/1tBs7ieWBVlZe7t6F5JAOWBry/tqQL/Vup4= | github.com/wwt/guac v1.3.2 h1:sH6OFGa/1tBs7ieWBVlZe7t6F5JAOWBry/tqQL/Vup4= | ||||||
| @ -273,6 +282,7 @@ github.com/wwt/guac v1.3.2/go.mod h1:eKm+NrnK7A88l4UBEcYNpZQGMpZRryYKoz4D/0/n1C0 | |||||||
| github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= | github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= | ||||||
| github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= | ||||||
| github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= | github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= | ||||||
|  | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= | ||||||
| go.mongodb.org/mongo-driver v1.14.0 h1:P98w8egYRjYe3XDjxhYJagTokP/H6HzlsnojRgZRd80= | go.mongodb.org/mongo-driver v1.14.0 h1:P98w8egYRjYe3XDjxhYJagTokP/H6HzlsnojRgZRd80= | ||||||
| go.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c= | go.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c= | ||||||
| go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= | ||||||
| @ -290,14 +300,20 @@ go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y | |||||||
| go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= | go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= | ||||||
| go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= | ||||||
| go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= | ||||||
| goauthentik.io/api/v3 v3.2025024.8 h1:2mG4CqGSsmZq2CtRehxpDjsER43U/JQSoTOn5VC1ui4= | goauthentik.io/api/v3 v3.2025024.7 h1:OOBuyLzv+l5rtvrOYzoDs6Hy9cIfkE5sewRqR5ThSRc= | ||||||
| goauthentik.io/api/v3 v3.2025024.8/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw= | goauthentik.io/api/v3 v3.2025024.7/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw= | ||||||
| golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= | ||||||
| golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= | ||||||
| golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= | ||||||
| golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= | ||||||
| golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= | ||||||
| golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= | golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= | ||||||
|  | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= | ||||||
|  | golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= | ||||||
|  | golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= | ||||||
|  | golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= | ||||||
|  | golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= | ||||||
|  | golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= | ||||||
| golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= | golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= | ||||||
| golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= | golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= | ||||||
| golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= | ||||||
| @ -332,6 +348,11 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB | |||||||
| golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= | golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= | ||||||
| golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= | ||||||
| golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= | ||||||
|  | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= | ||||||
|  | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= | ||||||
|  | golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= | ||||||
|  | golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= | ||||||
|  | golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= | ||||||
| golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= | ||||||
| golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= | ||||||
| golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= | ||||||
| @ -358,8 +379,17 @@ golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/ | |||||||
| golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= | golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= | ||||||
| golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= | golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= | ||||||
| golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= | golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= | ||||||
| golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= | ||||||
| golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= | ||||||
|  | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= | ||||||
|  | golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= | ||||||
|  | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= | ||||||
|  | golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= | ||||||
|  | golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= | ||||||
|  | golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= | ||||||
|  | golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= | ||||||
|  | golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= | ||||||
|  | golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= | ||||||
| golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= | ||||||
| golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= | ||||||
| golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= | ||||||
| @ -376,6 +406,12 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ | |||||||
| golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||||
| golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||||
| golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||||
|  | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||||
|  | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||||
|  | golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= | ||||||
|  | golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= | ||||||
|  | golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= | ||||||
|  | golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= | ||||||
| golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= | golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= | ||||||
| golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= | golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= | ||||||
| golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | ||||||
| @ -404,14 +440,40 @@ golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7w | |||||||
| golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||||
| golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||||
| golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||||
|  | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||||
|  | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||||
|  | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||||
| golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||||
|  | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||||
|  | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||||
|  | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||||
|  | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||||
|  | golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= | ||||||
|  | golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= | ||||||
|  | golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= | ||||||
| golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= | ||||||
| golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= | ||||||
|  | golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= | ||||||
|  | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= | ||||||
|  | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= | ||||||
|  | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= | ||||||
|  | golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= | ||||||
|  | golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= | ||||||
|  | golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= | ||||||
|  | golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= | ||||||
|  | golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= | ||||||
| golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | ||||||
| golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | ||||||
| golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | ||||||
| golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= | ||||||
| golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= | ||||||
|  | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= | ||||||
|  | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= | ||||||
|  | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= | ||||||
|  | golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= | ||||||
|  | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= | ||||||
|  | golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= | ||||||
|  | golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= | ||||||
| golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= | golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= | ||||||
| golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= | golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= | ||||||
| golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= | ||||||
| @ -457,6 +519,10 @@ golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roY | |||||||
| golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= | golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= | ||||||
| golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= | golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= | ||||||
| golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= | golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= | ||||||
|  | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= | ||||||
|  | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= | ||||||
|  | golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= | ||||||
|  | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= | ||||||
| golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||||||
| golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||||||
| golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||||||
|  | |||||||
| @ -8,7 +8,7 @@ msgid "" | |||||||
| msgstr "" | msgstr "" | ||||||
| "Project-Id-Version: PACKAGE VERSION\n" | "Project-Id-Version: PACKAGE VERSION\n" | ||||||
| "Report-Msgid-Bugs-To: \n" | "Report-Msgid-Bugs-To: \n" | ||||||
| "POT-Creation-Date: 2025-04-22 13:40+0000\n" | "POT-Creation-Date: 2025-04-17 00:09+0000\n" | ||||||
| "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" | ||||||
| "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" | "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" | ||||||
| "Language-Team: LANGUAGE <LL@li.org>\n" | "Language-Team: LANGUAGE <LL@li.org>\n" | ||||||
| @ -451,36 +451,6 @@ msgstr "" | |||||||
| msgid "License Usage Records" | msgid "License Usage Records" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: authentik/enterprise/policies/unique_password/models.py |  | ||||||
| #: authentik/policies/password/models.py |  | ||||||
| msgid "Field key to check, field keys defined in Prompt stages are available." |  | ||||||
| msgstr "" |  | ||||||
|  |  | ||||||
| #: authentik/enterprise/policies/unique_password/models.py |  | ||||||
| msgid "Number of passwords to check against." |  | ||||||
| msgstr "" |  | ||||||
|  |  | ||||||
| #: authentik/enterprise/policies/unique_password/models.py |  | ||||||
| #: authentik/policies/password/models.py |  | ||||||
| msgid "Password not set in context" |  | ||||||
| msgstr "" |  | ||||||
|  |  | ||||||
| #: authentik/enterprise/policies/unique_password/models.py |  | ||||||
| msgid "This password has been used previously. Please choose a different one." |  | ||||||
| msgstr "" |  | ||||||
|  |  | ||||||
| #: authentik/enterprise/policies/unique_password/models.py |  | ||||||
| msgid "Password Uniqueness Policy" |  | ||||||
| msgstr "" |  | ||||||
|  |  | ||||||
| #: authentik/enterprise/policies/unique_password/models.py |  | ||||||
| msgid "Password Uniqueness Policies" |  | ||||||
| msgstr "" |  | ||||||
|  |  | ||||||
| #: authentik/enterprise/policies/unique_password/models.py |  | ||||||
| msgid "User Password History" |  | ||||||
| msgstr "" |  | ||||||
|  |  | ||||||
| #: authentik/enterprise/policy.py | #: authentik/enterprise/policy.py | ||||||
| msgid "Enterprise required to access this feature." | msgid "Enterprise required to access this feature." | ||||||
| msgstr "" | msgstr "" | ||||||
| @ -1205,6 +1175,10 @@ msgstr "" | |||||||
| msgid "Clear Policy's cache metrics" | msgid "Clear Policy's cache metrics" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
|  | #: authentik/policies/password/models.py | ||||||
|  | msgid "Field key to check, field keys defined in Prompt stages are available." | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: authentik/policies/password/models.py | #: authentik/policies/password/models.py | ||||||
| msgid "How many times the password hash is allowed to be on haveibeenpwned" | msgid "How many times the password hash is allowed to be on haveibeenpwned" | ||||||
| msgstr "" | msgstr "" | ||||||
| @ -1214,6 +1188,10 @@ msgid "" | |||||||
| "If the zxcvbn score is equal or less than this value, the policy will fail." | "If the zxcvbn score is equal or less than this value, the policy will fail." | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
|  | #: authentik/policies/password/models.py | ||||||
|  | msgid "Password not set in context" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: authentik/policies/password/models.py | #: authentik/policies/password/models.py | ||||||
| msgid "Invalid password." | msgid "Invalid password." | ||||||
| msgstr "" | msgstr "" | ||||||
| @ -3166,12 +3144,6 @@ msgid "" | |||||||
| "info is entered." | "info is entered." | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: authentik/stages/identification/models.py |  | ||||||
| msgid "" |  | ||||||
| "Show the user the 'Remember me on this device' toggle, allowing repeat users " |  | ||||||
| "to skip straight to entering their password." |  | ||||||
| msgstr "" |  | ||||||
|  |  | ||||||
| #: authentik/stages/identification/models.py | #: authentik/stages/identification/models.py | ||||||
| msgid "Optional enrollment flow, which is linked at the bottom of the page." | msgid "Optional enrollment flow, which is linked at the bottom of the page." | ||||||
| msgstr "" | msgstr "" | ||||||
|  | |||||||
										
											Binary file not shown.
										
									
								
							| @ -19,7 +19,7 @@ msgid "" | |||||||
| msgstr "" | msgstr "" | ||||||
| "Project-Id-Version: PACKAGE VERSION\n" | "Project-Id-Version: PACKAGE VERSION\n" | ||||||
| "Report-Msgid-Bugs-To: \n" | "Report-Msgid-Bugs-To: \n" | ||||||
| "POT-Creation-Date: 2025-04-17 00:09+0000\n" | "POT-Creation-Date: 2025-04-15 00:11+0000\n" | ||||||
| "PO-Revision-Date: 2022-09-26 16:47+0000\n" | "PO-Revision-Date: 2022-09-26 16:47+0000\n" | ||||||
| "Last-Translator: Marc Schmitt, 2025\n" | "Last-Translator: Marc Schmitt, 2025\n" | ||||||
| "Language-Team: French (https://app.transifex.com/authentik/teams/119923/fr/)\n" | "Language-Team: French (https://app.transifex.com/authentik/teams/119923/fr/)\n" | ||||||
| @ -2508,14 +2508,6 @@ msgstr "Le mot de passe ne correspond pas à la complexité d'Active Directory." | |||||||
| msgid "No token received." | msgid "No token received." | ||||||
| msgstr "Pas de jeton reçu." | msgstr "Pas de jeton reçu." | ||||||
|  |  | ||||||
| #: authentik/sources/oauth/models.py |  | ||||||
| msgid "HTTP Basic Authentication" |  | ||||||
| msgstr "Authentification HTTP Basic" |  | ||||||
|  |  | ||||||
| #: authentik/sources/oauth/models.py |  | ||||||
| msgid "Include the client ID and secret as request parameters" |  | ||||||
| msgstr "Inclure le client ID et secret comme paramètres de la requête" |  | ||||||
|  |  | ||||||
| #: authentik/sources/oauth/models.py | #: authentik/sources/oauth/models.py | ||||||
| msgid "Request Token URL" | msgid "Request Token URL" | ||||||
| msgstr "URL du jeton de requête" | msgstr "URL du jeton de requête" | ||||||
| @ -2557,14 +2549,6 @@ msgstr "" | |||||||
| msgid "Additional Scopes" | msgid "Additional Scopes" | ||||||
| msgstr "Portées additionnelles" | msgstr "Portées additionnelles" | ||||||
|  |  | ||||||
| #: authentik/sources/oauth/models.py |  | ||||||
| msgid "" |  | ||||||
| "How to perform authentication during an authorization_code token request " |  | ||||||
| "flow" |  | ||||||
| msgstr "" |  | ||||||
| "Comment effectuer l'authentification lors d'une demande de jeton pour le " |  | ||||||
| "flux authorization_code" |  | ||||||
|  |  | ||||||
| #: authentik/sources/oauth/models.py | #: authentik/sources/oauth/models.py | ||||||
| msgid "OAuth Source" | msgid "OAuth Source" | ||||||
| msgstr "Source OAuth" | msgstr "Source OAuth" | ||||||
|  | |||||||
| @ -15,7 +15,7 @@ msgid "" | |||||||
| msgstr "" | msgstr "" | ||||||
| "Project-Id-Version: PACKAGE VERSION\n" | "Project-Id-Version: PACKAGE VERSION\n" | ||||||
| "Report-Msgid-Bugs-To: \n" | "Report-Msgid-Bugs-To: \n" | ||||||
| "POT-Creation-Date: 2025-04-18 00:09+0000\n" | "POT-Creation-Date: 2025-04-15 00:11+0000\n" | ||||||
| "PO-Revision-Date: 2022-09-26 16:47+0000\n" | "PO-Revision-Date: 2022-09-26 16:47+0000\n" | ||||||
| "Last-Translator: deluxghost, 2025\n" | "Last-Translator: deluxghost, 2025\n" | ||||||
| "Language-Team: Chinese Simplified (https://app.transifex.com/authentik/teams/119923/zh-Hans/)\n" | "Language-Team: Chinese Simplified (https://app.transifex.com/authentik/teams/119923/zh-Hans/)\n" | ||||||
| @ -2286,14 +2286,6 @@ msgstr "密码与 Active Directory 复杂度不匹配。" | |||||||
| msgid "No token received." | msgid "No token received." | ||||||
| msgstr "未收到令牌。" | msgstr "未收到令牌。" | ||||||
|  |  | ||||||
| #: authentik/sources/oauth/models.py |  | ||||||
| msgid "HTTP Basic Authentication" |  | ||||||
| msgstr "HTTP 基本身份验证" |  | ||||||
|  |  | ||||||
| #: authentik/sources/oauth/models.py |  | ||||||
| msgid "Include the client ID and secret as request parameters" |  | ||||||
| msgstr "包括客户端 ID 和密钥作为请求参数" |  | ||||||
|  |  | ||||||
| #: authentik/sources/oauth/models.py | #: authentik/sources/oauth/models.py | ||||||
| msgid "Request Token URL" | msgid "Request Token URL" | ||||||
| msgstr "请求令牌 URL" | msgstr "请求令牌 URL" | ||||||
| @ -2332,12 +2324,6 @@ msgstr "authentik 用来获取用户信息的 URL。" | |||||||
| msgid "Additional Scopes" | msgid "Additional Scopes" | ||||||
| msgstr "额外的作用域" | msgstr "额外的作用域" | ||||||
|  |  | ||||||
| #: authentik/sources/oauth/models.py |  | ||||||
| msgid "" |  | ||||||
| "How to perform authentication during an authorization_code token request " |  | ||||||
| "flow" |  | ||||||
| msgstr "在 authorization_code 令牌请求流程期间,如何执行身份验证" |  | ||||||
|  |  | ||||||
| #: authentik/sources/oauth/models.py | #: authentik/sources/oauth/models.py | ||||||
| msgid "OAuth Source" | msgid "OAuth Source" | ||||||
| msgstr "OAuth 源" | msgstr "OAuth 源" | ||||||
| @ -3208,12 +3194,6 @@ msgid "" | |||||||
| "info is entered." | "info is entered." | ||||||
| msgstr "启用时,即使输入错误的用户信息,此阶段也会成功并继续。" | msgstr "启用时,即使输入错误的用户信息,此阶段也会成功并继续。" | ||||||
|  |  | ||||||
| #: authentik/stages/identification/models.py |  | ||||||
| msgid "" |  | ||||||
| "Show the user the 'Remember me on this device' toggle, allowing repeat users" |  | ||||||
| " to skip straight to entering their password." |  | ||||||
| msgstr "向用户显示“在此设备上记住我”开关,允许相同用户直接跳过输入密码。" |  | ||||||
|  |  | ||||||
| #: authentik/stages/identification/models.py | #: authentik/stages/identification/models.py | ||||||
| msgid "Optional enrollment flow, which is linked at the bottom of the page." | msgid "Optional enrollment flow, which is linked at the bottom of the page." | ||||||
| msgstr "可选注册流程,链接在页面底部。" | msgstr "可选注册流程,链接在页面底部。" | ||||||
|  | |||||||
										
											Binary file not shown.
										
									
								
							| @ -14,7 +14,7 @@ msgid "" | |||||||
| msgstr "" | msgstr "" | ||||||
| "Project-Id-Version: PACKAGE VERSION\n" | "Project-Id-Version: PACKAGE VERSION\n" | ||||||
| "Report-Msgid-Bugs-To: \n" | "Report-Msgid-Bugs-To: \n" | ||||||
| "POT-Creation-Date: 2025-04-18 00:09+0000\n" | "POT-Creation-Date: 2025-04-15 00:11+0000\n" | ||||||
| "PO-Revision-Date: 2022-09-26 16:47+0000\n" | "PO-Revision-Date: 2022-09-26 16:47+0000\n" | ||||||
| "Last-Translator: deluxghost, 2025\n" | "Last-Translator: deluxghost, 2025\n" | ||||||
| "Language-Team: Chinese (China) (https://app.transifex.com/authentik/teams/119923/zh_CN/)\n" | "Language-Team: Chinese (China) (https://app.transifex.com/authentik/teams/119923/zh_CN/)\n" | ||||||
| @ -2285,14 +2285,6 @@ msgstr "密码与 Active Directory 复杂度不匹配。" | |||||||
| msgid "No token received." | msgid "No token received." | ||||||
| msgstr "未收到令牌。" | msgstr "未收到令牌。" | ||||||
|  |  | ||||||
| #: authentik/sources/oauth/models.py |  | ||||||
| msgid "HTTP Basic Authentication" |  | ||||||
| msgstr "HTTP 基本身份验证" |  | ||||||
|  |  | ||||||
| #: authentik/sources/oauth/models.py |  | ||||||
| msgid "Include the client ID and secret as request parameters" |  | ||||||
| msgstr "包括客户端 ID 和密钥作为请求参数" |  | ||||||
|  |  | ||||||
| #: authentik/sources/oauth/models.py | #: authentik/sources/oauth/models.py | ||||||
| msgid "Request Token URL" | msgid "Request Token URL" | ||||||
| msgstr "请求令牌 URL" | msgstr "请求令牌 URL" | ||||||
| @ -2331,12 +2323,6 @@ msgstr "authentik 用来获取用户信息的 URL。" | |||||||
| msgid "Additional Scopes" | msgid "Additional Scopes" | ||||||
| msgstr "额外的作用域" | msgstr "额外的作用域" | ||||||
|  |  | ||||||
| #: authentik/sources/oauth/models.py |  | ||||||
| msgid "" |  | ||||||
| "How to perform authentication during an authorization_code token request " |  | ||||||
| "flow" |  | ||||||
| msgstr "在 authorization_code 令牌请求流程期间,如何执行身份验证" |  | ||||||
|  |  | ||||||
| #: authentik/sources/oauth/models.py | #: authentik/sources/oauth/models.py | ||||||
| msgid "OAuth Source" | msgid "OAuth Source" | ||||||
| msgstr "OAuth 源" | msgstr "OAuth 源" | ||||||
| @ -3207,12 +3193,6 @@ msgid "" | |||||||
| "info is entered." | "info is entered." | ||||||
| msgstr "启用时,即使输入错误的用户信息,此阶段也会成功并继续。" | msgstr "启用时,即使输入错误的用户信息,此阶段也会成功并继续。" | ||||||
|  |  | ||||||
| #: authentik/stages/identification/models.py |  | ||||||
| msgid "" |  | ||||||
| "Show the user the 'Remember me on this device' toggle, allowing repeat users" |  | ||||||
| " to skip straight to entering their password." |  | ||||||
| msgstr "向用户显示“在此设备上记住我”开关,允许相同用户直接跳过输入密码。" |  | ||||||
|  |  | ||||||
| #: authentik/stages/identification/models.py | #: authentik/stages/identification/models.py | ||||||
| msgid "Optional enrollment flow, which is linked at the bottom of the page." | msgid "Optional enrollment flow, which is linked at the bottom of the page." | ||||||
| msgstr "可选注册流程,链接在页面底部。" | msgstr "可选注册流程,链接在页面底部。" | ||||||
|  | |||||||
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										424
									
								
								schema.yml
									
									
									
									
									
								
							
							
						
						
									
										424
									
								
								schema.yml
									
									
									
									
									
								
							| @ -12092,6 +12092,10 @@ paths: | |||||||
|         name: enabled |         name: enabled | ||||||
|         schema: |         schema: | ||||||
|           type: boolean |           type: boolean | ||||||
|  |       - in: query | ||||||
|  |         name: honor_order | ||||||
|  |         schema: | ||||||
|  |           type: boolean | ||||||
|       - in: query |       - in: query | ||||||
|         name: order |         name: order | ||||||
|         schema: |         schema: | ||||||
| @ -14721,302 +14725,6 @@ paths: | |||||||
|               schema: |               schema: | ||||||
|                 $ref: '#/components/schemas/GenericError' |                 $ref: '#/components/schemas/GenericError' | ||||||
|           description: '' |           description: '' | ||||||
|   /policies/unique_password/: |  | ||||||
|     get: |  | ||||||
|       operationId: policies_unique_password_list |  | ||||||
|       description: Password Uniqueness Policy Viewset |  | ||||||
|       parameters: |  | ||||||
|       - in: query |  | ||||||
|         name: created |  | ||||||
|         schema: |  | ||||||
|           type: string |  | ||||||
|           format: date-time |  | ||||||
|       - in: query |  | ||||||
|         name: execution_logging |  | ||||||
|         schema: |  | ||||||
|           type: boolean |  | ||||||
|       - in: query |  | ||||||
|         name: last_updated |  | ||||||
|         schema: |  | ||||||
|           type: string |  | ||||||
|           format: date-time |  | ||||||
|       - in: query |  | ||||||
|         name: name |  | ||||||
|         schema: |  | ||||||
|           type: string |  | ||||||
|       - in: query |  | ||||||
|         name: num_historical_passwords |  | ||||||
|         schema: |  | ||||||
|           type: integer |  | ||||||
|       - name: ordering |  | ||||||
|         required: false |  | ||||||
|         in: query |  | ||||||
|         description: Which field to use when ordering the results. |  | ||||||
|         schema: |  | ||||||
|           type: string |  | ||||||
|       - name: page |  | ||||||
|         required: false |  | ||||||
|         in: query |  | ||||||
|         description: A page number within the paginated result set. |  | ||||||
|         schema: |  | ||||||
|           type: integer |  | ||||||
|       - name: page_size |  | ||||||
|         required: false |  | ||||||
|         in: query |  | ||||||
|         description: Number of results to return per page. |  | ||||||
|         schema: |  | ||||||
|           type: integer |  | ||||||
|       - in: query |  | ||||||
|         name: password_field |  | ||||||
|         schema: |  | ||||||
|           type: string |  | ||||||
|       - in: query |  | ||||||
|         name: policy_uuid |  | ||||||
|         schema: |  | ||||||
|           type: string |  | ||||||
|           format: uuid |  | ||||||
|       - name: search |  | ||||||
|         required: false |  | ||||||
|         in: query |  | ||||||
|         description: A search term. |  | ||||||
|         schema: |  | ||||||
|           type: string |  | ||||||
|       tags: |  | ||||||
|       - policies |  | ||||||
|       security: |  | ||||||
|       - authentik: [] |  | ||||||
|       responses: |  | ||||||
|         '200': |  | ||||||
|           content: |  | ||||||
|             application/json: |  | ||||||
|               schema: |  | ||||||
|                 $ref: '#/components/schemas/PaginatedUniquePasswordPolicyList' |  | ||||||
|           description: '' |  | ||||||
|         '400': |  | ||||||
|           content: |  | ||||||
|             application/json: |  | ||||||
|               schema: |  | ||||||
|                 $ref: '#/components/schemas/ValidationError' |  | ||||||
|           description: '' |  | ||||||
|         '403': |  | ||||||
|           content: |  | ||||||
|             application/json: |  | ||||||
|               schema: |  | ||||||
|                 $ref: '#/components/schemas/GenericError' |  | ||||||
|           description: '' |  | ||||||
|     post: |  | ||||||
|       operationId: policies_unique_password_create |  | ||||||
|       description: Password Uniqueness Policy Viewset |  | ||||||
|       tags: |  | ||||||
|       - policies |  | ||||||
|       requestBody: |  | ||||||
|         content: |  | ||||||
|           application/json: |  | ||||||
|             schema: |  | ||||||
|               $ref: '#/components/schemas/UniquePasswordPolicyRequest' |  | ||||||
|         required: true |  | ||||||
|       security: |  | ||||||
|       - authentik: [] |  | ||||||
|       responses: |  | ||||||
|         '201': |  | ||||||
|           content: |  | ||||||
|             application/json: |  | ||||||
|               schema: |  | ||||||
|                 $ref: '#/components/schemas/UniquePasswordPolicy' |  | ||||||
|           description: '' |  | ||||||
|         '400': |  | ||||||
|           content: |  | ||||||
|             application/json: |  | ||||||
|               schema: |  | ||||||
|                 $ref: '#/components/schemas/ValidationError' |  | ||||||
|           description: '' |  | ||||||
|         '403': |  | ||||||
|           content: |  | ||||||
|             application/json: |  | ||||||
|               schema: |  | ||||||
|                 $ref: '#/components/schemas/GenericError' |  | ||||||
|           description: '' |  | ||||||
|   /policies/unique_password/{policy_uuid}/: |  | ||||||
|     get: |  | ||||||
|       operationId: policies_unique_password_retrieve |  | ||||||
|       description: Password Uniqueness Policy Viewset |  | ||||||
|       parameters: |  | ||||||
|       - in: path |  | ||||||
|         name: policy_uuid |  | ||||||
|         schema: |  | ||||||
|           type: string |  | ||||||
|           format: uuid |  | ||||||
|         description: A UUID string identifying this Password Uniqueness Policy. |  | ||||||
|         required: true |  | ||||||
|       tags: |  | ||||||
|       - policies |  | ||||||
|       security: |  | ||||||
|       - authentik: [] |  | ||||||
|       responses: |  | ||||||
|         '200': |  | ||||||
|           content: |  | ||||||
|             application/json: |  | ||||||
|               schema: |  | ||||||
|                 $ref: '#/components/schemas/UniquePasswordPolicy' |  | ||||||
|           description: '' |  | ||||||
|         '400': |  | ||||||
|           content: |  | ||||||
|             application/json: |  | ||||||
|               schema: |  | ||||||
|                 $ref: '#/components/schemas/ValidationError' |  | ||||||
|           description: '' |  | ||||||
|         '403': |  | ||||||
|           content: |  | ||||||
|             application/json: |  | ||||||
|               schema: |  | ||||||
|                 $ref: '#/components/schemas/GenericError' |  | ||||||
|           description: '' |  | ||||||
|     put: |  | ||||||
|       operationId: policies_unique_password_update |  | ||||||
|       description: Password Uniqueness Policy Viewset |  | ||||||
|       parameters: |  | ||||||
|       - in: path |  | ||||||
|         name: policy_uuid |  | ||||||
|         schema: |  | ||||||
|           type: string |  | ||||||
|           format: uuid |  | ||||||
|         description: A UUID string identifying this Password Uniqueness Policy. |  | ||||||
|         required: true |  | ||||||
|       tags: |  | ||||||
|       - policies |  | ||||||
|       requestBody: |  | ||||||
|         content: |  | ||||||
|           application/json: |  | ||||||
|             schema: |  | ||||||
|               $ref: '#/components/schemas/UniquePasswordPolicyRequest' |  | ||||||
|         required: true |  | ||||||
|       security: |  | ||||||
|       - authentik: [] |  | ||||||
|       responses: |  | ||||||
|         '200': |  | ||||||
|           content: |  | ||||||
|             application/json: |  | ||||||
|               schema: |  | ||||||
|                 $ref: '#/components/schemas/UniquePasswordPolicy' |  | ||||||
|           description: '' |  | ||||||
|         '400': |  | ||||||
|           content: |  | ||||||
|             application/json: |  | ||||||
|               schema: |  | ||||||
|                 $ref: '#/components/schemas/ValidationError' |  | ||||||
|           description: '' |  | ||||||
|         '403': |  | ||||||
|           content: |  | ||||||
|             application/json: |  | ||||||
|               schema: |  | ||||||
|                 $ref: '#/components/schemas/GenericError' |  | ||||||
|           description: '' |  | ||||||
|     patch: |  | ||||||
|       operationId: policies_unique_password_partial_update |  | ||||||
|       description: Password Uniqueness Policy Viewset |  | ||||||
|       parameters: |  | ||||||
|       - in: path |  | ||||||
|         name: policy_uuid |  | ||||||
|         schema: |  | ||||||
|           type: string |  | ||||||
|           format: uuid |  | ||||||
|         description: A UUID string identifying this Password Uniqueness Policy. |  | ||||||
|         required: true |  | ||||||
|       tags: |  | ||||||
|       - policies |  | ||||||
|       requestBody: |  | ||||||
|         content: |  | ||||||
|           application/json: |  | ||||||
|             schema: |  | ||||||
|               $ref: '#/components/schemas/PatchedUniquePasswordPolicyRequest' |  | ||||||
|       security: |  | ||||||
|       - authentik: [] |  | ||||||
|       responses: |  | ||||||
|         '200': |  | ||||||
|           content: |  | ||||||
|             application/json: |  | ||||||
|               schema: |  | ||||||
|                 $ref: '#/components/schemas/UniquePasswordPolicy' |  | ||||||
|           description: '' |  | ||||||
|         '400': |  | ||||||
|           content: |  | ||||||
|             application/json: |  | ||||||
|               schema: |  | ||||||
|                 $ref: '#/components/schemas/ValidationError' |  | ||||||
|           description: '' |  | ||||||
|         '403': |  | ||||||
|           content: |  | ||||||
|             application/json: |  | ||||||
|               schema: |  | ||||||
|                 $ref: '#/components/schemas/GenericError' |  | ||||||
|           description: '' |  | ||||||
|     delete: |  | ||||||
|       operationId: policies_unique_password_destroy |  | ||||||
|       description: Password Uniqueness Policy Viewset |  | ||||||
|       parameters: |  | ||||||
|       - in: path |  | ||||||
|         name: policy_uuid |  | ||||||
|         schema: |  | ||||||
|           type: string |  | ||||||
|           format: uuid |  | ||||||
|         description: A UUID string identifying this Password Uniqueness Policy. |  | ||||||
|         required: true |  | ||||||
|       tags: |  | ||||||
|       - policies |  | ||||||
|       security: |  | ||||||
|       - authentik: [] |  | ||||||
|       responses: |  | ||||||
|         '204': |  | ||||||
|           description: No response body |  | ||||||
|         '400': |  | ||||||
|           content: |  | ||||||
|             application/json: |  | ||||||
|               schema: |  | ||||||
|                 $ref: '#/components/schemas/ValidationError' |  | ||||||
|           description: '' |  | ||||||
|         '403': |  | ||||||
|           content: |  | ||||||
|             application/json: |  | ||||||
|               schema: |  | ||||||
|                 $ref: '#/components/schemas/GenericError' |  | ||||||
|           description: '' |  | ||||||
|   /policies/unique_password/{policy_uuid}/used_by/: |  | ||||||
|     get: |  | ||||||
|       operationId: policies_unique_password_used_by_list |  | ||||||
|       description: Get a list of all objects that use this object |  | ||||||
|       parameters: |  | ||||||
|       - in: path |  | ||||||
|         name: policy_uuid |  | ||||||
|         schema: |  | ||||||
|           type: string |  | ||||||
|           format: uuid |  | ||||||
|         description: A UUID string identifying this Password Uniqueness Policy. |  | ||||||
|         required: true |  | ||||||
|       tags: |  | ||||||
|       - policies |  | ||||||
|       security: |  | ||||||
|       - authentik: [] |  | ||||||
|       responses: |  | ||||||
|         '200': |  | ||||||
|           content: |  | ||||||
|             application/json: |  | ||||||
|               schema: |  | ||||||
|                 type: array |  | ||||||
|                 items: |  | ||||||
|                   $ref: '#/components/schemas/UsedBy' |  | ||||||
|           description: '' |  | ||||||
|         '400': |  | ||||||
|           content: |  | ||||||
|             application/json: |  | ||||||
|               schema: |  | ||||||
|                 $ref: '#/components/schemas/ValidationError' |  | ||||||
|           description: '' |  | ||||||
|         '403': |  | ||||||
|           content: |  | ||||||
|             application/json: |  | ||||||
|               schema: |  | ||||||
|                 $ref: '#/components/schemas/GenericError' |  | ||||||
|           description: '' |  | ||||||
|   /propertymappings/all/: |   /propertymappings/all/: | ||||||
|     get: |     get: | ||||||
|       operationId: propertymappings_all_list |       operationId: propertymappings_all_list | ||||||
| @ -24912,7 +24620,6 @@ paths: | |||||||
|           - authentik_policies_geoip.geoippolicy |           - authentik_policies_geoip.geoippolicy | ||||||
|           - authentik_policies_password.passwordpolicy |           - authentik_policies_password.passwordpolicy | ||||||
|           - authentik_policies_reputation.reputationpolicy |           - authentik_policies_reputation.reputationpolicy | ||||||
|           - authentik_policies_unique_password.uniquepasswordpolicy |  | ||||||
|           - authentik_providers_google_workspace.googleworkspaceprovider |           - authentik_providers_google_workspace.googleworkspaceprovider | ||||||
|           - authentik_providers_google_workspace.googleworkspaceprovidermapping |           - authentik_providers_google_workspace.googleworkspaceprovidermapping | ||||||
|           - authentik_providers_ldap.ldapprovider |           - authentik_providers_ldap.ldapprovider | ||||||
| @ -25160,7 +24867,6 @@ paths: | |||||||
|           - authentik_policies_geoip.geoippolicy |           - authentik_policies_geoip.geoippolicy | ||||||
|           - authentik_policies_password.passwordpolicy |           - authentik_policies_password.passwordpolicy | ||||||
|           - authentik_policies_reputation.reputationpolicy |           - authentik_policies_reputation.reputationpolicy | ||||||
|           - authentik_policies_unique_password.uniquepasswordpolicy |  | ||||||
|           - authentik_providers_google_workspace.googleworkspaceprovider |           - authentik_providers_google_workspace.googleworkspaceprovider | ||||||
|           - authentik_providers_google_workspace.googleworkspaceprovidermapping |           - authentik_providers_google_workspace.googleworkspaceprovidermapping | ||||||
|           - authentik_providers_ldap.ldapprovider |           - authentik_providers_ldap.ldapprovider | ||||||
| @ -40941,7 +40647,6 @@ components: | |||||||
|       - authentik.core |       - authentik.core | ||||||
|       - authentik.enterprise |       - authentik.enterprise | ||||||
|       - authentik.enterprise.audit |       - authentik.enterprise.audit | ||||||
|       - authentik.enterprise.policies.unique_password |  | ||||||
|       - authentik.enterprise.providers.google_workspace |       - authentik.enterprise.providers.google_workspace | ||||||
|       - authentik.enterprise.providers.microsoft_entra |       - authentik.enterprise.providers.microsoft_entra | ||||||
|       - authentik.enterprise.providers.ssf |       - authentik.enterprise.providers.ssf | ||||||
| @ -48361,7 +48066,6 @@ components: | |||||||
|       - authentik_core.applicationentitlement |       - authentik_core.applicationentitlement | ||||||
|       - authentik_core.token |       - authentik_core.token | ||||||
|       - authentik_enterprise.license |       - authentik_enterprise.license | ||||||
|       - authentik_policies_unique_password.uniquepasswordpolicy |  | ||||||
|       - authentik_providers_google_workspace.googleworkspaceprovider |       - authentik_providers_google_workspace.googleworkspaceprovider | ||||||
|       - authentik_providers_google_workspace.googleworkspaceprovidermapping |       - authentik_providers_google_workspace.googleworkspaceprovidermapping | ||||||
|       - authentik_providers_microsoft_entra.microsoftentraprovider |       - authentik_providers_microsoft_entra.microsoftentraprovider | ||||||
| @ -50916,18 +50620,6 @@ components: | |||||||
|       required: |       required: | ||||||
|       - pagination |       - pagination | ||||||
|       - results |       - results | ||||||
|     PaginatedUniquePasswordPolicyList: |  | ||||||
|       type: object |  | ||||||
|       properties: |  | ||||||
|         pagination: |  | ||||||
|           $ref: '#/components/schemas/Pagination' |  | ||||||
|         results: |  | ||||||
|           type: array |  | ||||||
|           items: |  | ||||||
|             $ref: '#/components/schemas/UniquePasswordPolicy' |  | ||||||
|       required: |  | ||||||
|       - pagination |  | ||||||
|       - results |  | ||||||
|     PaginatedUserAssignedObjectPermissionList: |     PaginatedUserAssignedObjectPermissionList: | ||||||
|       type: object |       type: object | ||||||
|       properties: |       properties: | ||||||
| @ -53623,6 +53315,9 @@ components: | |||||||
|           type: integer |           type: integer | ||||||
|           maximum: 2147483647 |           maximum: 2147483647 | ||||||
|           minimum: -2147483648 |           minimum: -2147483648 | ||||||
|  |         honor_order: | ||||||
|  |           type: boolean | ||||||
|  |           description: Honor order when evaluating policies. | ||||||
|         timeout: |         timeout: | ||||||
|           type: integer |           type: integer | ||||||
|           maximum: 2147483647 |           maximum: 2147483647 | ||||||
| @ -54537,27 +54232,6 @@ components: | |||||||
|           nullable: true |           nullable: true | ||||||
|         expiring: |         expiring: | ||||||
|           type: boolean |           type: boolean | ||||||
|     PatchedUniquePasswordPolicyRequest: |  | ||||||
|       type: object |  | ||||||
|       description: Password Uniqueness Policy Serializer |  | ||||||
|       properties: |  | ||||||
|         name: |  | ||||||
|           type: string |  | ||||||
|           minLength: 1 |  | ||||||
|         execution_logging: |  | ||||||
|           type: boolean |  | ||||||
|           description: When this option is enabled, all executions of this policy |  | ||||||
|             will be logged. By default, only execution errors are logged. |  | ||||||
|         password_field: |  | ||||||
|           type: string |  | ||||||
|           minLength: 1 |  | ||||||
|           description: Field key to check, field keys defined in Prompt stages are |  | ||||||
|             available. |  | ||||||
|         num_historical_passwords: |  | ||||||
|           type: integer |  | ||||||
|           maximum: 2147483647 |  | ||||||
|           minimum: 0 |  | ||||||
|           description: Number of passwords to check against. |  | ||||||
|     PatchedUserDeleteStageRequest: |     PatchedUserDeleteStageRequest: | ||||||
|       type: object |       type: object | ||||||
|       description: UserDeleteStage Serializer |       description: UserDeleteStage Serializer | ||||||
| @ -55213,6 +54887,9 @@ components: | |||||||
|           type: integer |           type: integer | ||||||
|           maximum: 2147483647 |           maximum: 2147483647 | ||||||
|           minimum: -2147483648 |           minimum: -2147483648 | ||||||
|  |         honor_order: | ||||||
|  |           type: boolean | ||||||
|  |           description: Honor order when evaluating policies. | ||||||
|         timeout: |         timeout: | ||||||
|           type: integer |           type: integer | ||||||
|           maximum: 2147483647 |           maximum: 2147483647 | ||||||
| @ -55255,6 +54932,9 @@ components: | |||||||
|           type: integer |           type: integer | ||||||
|           maximum: 2147483647 |           maximum: 2147483647 | ||||||
|           minimum: -2147483648 |           minimum: -2147483648 | ||||||
|  |         honor_order: | ||||||
|  |           type: boolean | ||||||
|  |           description: Honor order when evaluating policies. | ||||||
|         timeout: |         timeout: | ||||||
|           type: integer |           type: integer | ||||||
|           maximum: 2147483647 |           maximum: 2147483647 | ||||||
| @ -59516,6 +59196,9 @@ components: | |||||||
|           type: integer |           type: integer | ||||||
|           maximum: 2147483647 |           maximum: 2147483647 | ||||||
|           minimum: -2147483648 |           minimum: -2147483648 | ||||||
|  |         honor_order: | ||||||
|  |           type: boolean | ||||||
|  |           description: Honor order when evaluating policies. | ||||||
|         timeout: |         timeout: | ||||||
|           type: integer |           type: integer | ||||||
|           maximum: 2147483647 |           maximum: 2147483647 | ||||||
| @ -59554,81 +59237,6 @@ components: | |||||||
|       - light |       - light | ||||||
|       - dark |       - dark | ||||||
|       type: string |       type: string | ||||||
|     UniquePasswordPolicy: |  | ||||||
|       type: object |  | ||||||
|       description: Password Uniqueness Policy Serializer |  | ||||||
|       properties: |  | ||||||
|         pk: |  | ||||||
|           type: string |  | ||||||
|           format: uuid |  | ||||||
|           readOnly: true |  | ||||||
|           title: Policy uuid |  | ||||||
|         name: |  | ||||||
|           type: string |  | ||||||
|         execution_logging: |  | ||||||
|           type: boolean |  | ||||||
|           description: When this option is enabled, all executions of this policy |  | ||||||
|             will be logged. By default, only execution errors are logged. |  | ||||||
|         component: |  | ||||||
|           type: string |  | ||||||
|           description: Get object component so that we know how to edit the object |  | ||||||
|           readOnly: true |  | ||||||
|         verbose_name: |  | ||||||
|           type: string |  | ||||||
|           description: Return object's verbose_name |  | ||||||
|           readOnly: true |  | ||||||
|         verbose_name_plural: |  | ||||||
|           type: string |  | ||||||
|           description: Return object's plural verbose_name |  | ||||||
|           readOnly: true |  | ||||||
|         meta_model_name: |  | ||||||
|           type: string |  | ||||||
|           description: Return internal model name |  | ||||||
|           readOnly: true |  | ||||||
|         bound_to: |  | ||||||
|           type: integer |  | ||||||
|           description: Return objects policy is bound to |  | ||||||
|           readOnly: true |  | ||||||
|         password_field: |  | ||||||
|           type: string |  | ||||||
|           description: Field key to check, field keys defined in Prompt stages are |  | ||||||
|             available. |  | ||||||
|         num_historical_passwords: |  | ||||||
|           type: integer |  | ||||||
|           maximum: 2147483647 |  | ||||||
|           minimum: 0 |  | ||||||
|           description: Number of passwords to check against. |  | ||||||
|       required: |  | ||||||
|       - bound_to |  | ||||||
|       - component |  | ||||||
|       - meta_model_name |  | ||||||
|       - name |  | ||||||
|       - pk |  | ||||||
|       - verbose_name |  | ||||||
|       - verbose_name_plural |  | ||||||
|     UniquePasswordPolicyRequest: |  | ||||||
|       type: object |  | ||||||
|       description: Password Uniqueness Policy Serializer |  | ||||||
|       properties: |  | ||||||
|         name: |  | ||||||
|           type: string |  | ||||||
|           minLength: 1 |  | ||||||
|         execution_logging: |  | ||||||
|           type: boolean |  | ||||||
|           description: When this option is enabled, all executions of this policy |  | ||||||
|             will be logged. By default, only execution errors are logged. |  | ||||||
|         password_field: |  | ||||||
|           type: string |  | ||||||
|           minLength: 1 |  | ||||||
|           description: Field key to check, field keys defined in Prompt stages are |  | ||||||
|             available. |  | ||||||
|         num_historical_passwords: |  | ||||||
|           type: integer |  | ||||||
|           maximum: 2147483647 |  | ||||||
|           minimum: 0 |  | ||||||
|           description: Number of passwords to check against. |  | ||||||
|       required: |  | ||||||
|       - name |  | ||||||
|     UsedBy: |     UsedBy: | ||||||
|       type: object |       type: object | ||||||
|       description: A list of all objects referencing the queried object |       description: A list of all objects referencing the queried object | ||||||
|  | |||||||
| @ -410,3 +410,77 @@ class TestProviderOAuth2OAuth(SeleniumTestCase): | |||||||
|             self.driver.find_element(By.CSS_SELECTOR, "header > h1").text, |             self.driver.find_element(By.CSS_SELECTOR, "header > h1").text, | ||||||
|             "Permission denied", |             "Permission denied", | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|  |     @retry() | ||||||
|  |     @apply_blueprint( | ||||||
|  |         "default/flow-default-authentication-flow.yaml", | ||||||
|  |         "default/flow-default-invalidation-flow.yaml", | ||||||
|  |     ) | ||||||
|  |     @apply_blueprint("default/flow-default-provider-authorization-implicit-consent.yaml") | ||||||
|  |     @apply_blueprint("system/providers-oauth2.yaml") | ||||||
|  |     @reconcile_app("authentik_crypto") | ||||||
|  |     def test_authorization_consent_implied_parallel(self): | ||||||
|  |         """test OpenID Provider flow (default authorization flow with implied consent)""" | ||||||
|  |         # Bootstrap all needed objects | ||||||
|  |         authorization_flow = Flow.objects.get( | ||||||
|  |             slug="default-provider-authorization-implicit-consent" | ||||||
|  |         ) | ||||||
|  |         provider = OAuth2Provider.objects.create( | ||||||
|  |             name=generate_id(), | ||||||
|  |             client_type=ClientTypes.CONFIDENTIAL, | ||||||
|  |             client_id=self.client_id, | ||||||
|  |             client_secret=self.client_secret, | ||||||
|  |             signing_key=create_test_cert(), | ||||||
|  |             redirect_uris=[ | ||||||
|  |                 RedirectURI( | ||||||
|  |                     RedirectURIMatchingMode.STRICT, "http://localhost:3000/login/generic_oauth" | ||||||
|  |                 ) | ||||||
|  |             ], | ||||||
|  |             authorization_flow=authorization_flow, | ||||||
|  |         ) | ||||||
|  |         provider.property_mappings.set( | ||||||
|  |             ScopeMapping.objects.filter( | ||||||
|  |                 scope_name__in=[ | ||||||
|  |                     SCOPE_OPENID, | ||||||
|  |                     SCOPE_OPENID_EMAIL, | ||||||
|  |                     SCOPE_OPENID_PROFILE, | ||||||
|  |                     SCOPE_OFFLINE_ACCESS, | ||||||
|  |                 ] | ||||||
|  |             ) | ||||||
|  |         ) | ||||||
|  |         Application.objects.create( | ||||||
|  |             name=generate_id(), | ||||||
|  |             slug=self.app_slug, | ||||||
|  |             provider=provider, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         self.driver.get(self.live_server_url) | ||||||
|  |         login_window = self.driver.current_window_handle | ||||||
|  |  | ||||||
|  |         self.driver.switch_to.new_window("tab") | ||||||
|  |         grafana_window = self.driver.current_window_handle | ||||||
|  |         self.driver.get("http://localhost:3000") | ||||||
|  |         self.driver.find_element(By.CLASS_NAME, "btn-service--oauth").click() | ||||||
|  |  | ||||||
|  |         self.driver.switch_to.window(login_window) | ||||||
|  |         self.login() | ||||||
|  |  | ||||||
|  |         self.driver.switch_to.window(grafana_window) | ||||||
|  |         self.wait_for_url("http://localhost:3000/?orgId=1") | ||||||
|  |         self.driver.get("http://localhost:3000/profile") | ||||||
|  |         self.assertEqual( | ||||||
|  |             self.driver.find_element(By.CLASS_NAME, "page-header__title").text, | ||||||
|  |             self.user.name, | ||||||
|  |         ) | ||||||
|  |         self.assertEqual( | ||||||
|  |             self.driver.find_element(By.CSS_SELECTOR, "input[name=name]").get_attribute("value"), | ||||||
|  |             self.user.name, | ||||||
|  |         ) | ||||||
|  |         self.assertEqual( | ||||||
|  |             self.driver.find_element(By.CSS_SELECTOR, "input[name=email]").get_attribute("value"), | ||||||
|  |             self.user.email, | ||||||
|  |         ) | ||||||
|  |         self.assertEqual( | ||||||
|  |             self.driver.find_element(By.CSS_SELECTOR, "input[name=login]").get_attribute("value"), | ||||||
|  |             self.user.email, | ||||||
|  |         ) | ||||||
|  | |||||||
| @ -20,7 +20,7 @@ from tests.e2e.utils import SeleniumTestCase, retry | |||||||
| class TestProviderSAML(SeleniumTestCase): | class TestProviderSAML(SeleniumTestCase): | ||||||
|     """test SAML Provider flow""" |     """test SAML Provider flow""" | ||||||
|  |  | ||||||
|     def setup_client(self, provider: SAMLProvider, force_post: bool = False): |     def setup_client(self, provider: SAMLProvider, force_post: bool = False, **kwargs): | ||||||
|         """Setup client saml-sp container which we test SAML against""" |         """Setup client saml-sp container which we test SAML against""" | ||||||
|         metadata_url = ( |         metadata_url = ( | ||||||
|             self.url( |             self.url( | ||||||
| @ -40,6 +40,7 @@ class TestProviderSAML(SeleniumTestCase): | |||||||
|                 "SP_ENTITY_ID": provider.issuer, |                 "SP_ENTITY_ID": provider.issuer, | ||||||
|                 "SP_SSO_BINDING": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST", |                 "SP_SSO_BINDING": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST", | ||||||
|                 "SP_METADATA_URL": metadata_url, |                 "SP_METADATA_URL": metadata_url, | ||||||
|  |                 **kwargs, | ||||||
|             }, |             }, | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
| @ -111,6 +112,74 @@ class TestProviderSAML(SeleniumTestCase): | |||||||
|             [self.user.email], |             [self.user.email], | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|  |     @retry() | ||||||
|  |     @apply_blueprint( | ||||||
|  |         "default/flow-default-authentication-flow.yaml", | ||||||
|  |         "default/flow-default-invalidation-flow.yaml", | ||||||
|  |     ) | ||||||
|  |     @apply_blueprint( | ||||||
|  |         "default/flow-default-provider-authorization-implicit-consent.yaml", | ||||||
|  |     ) | ||||||
|  |     @apply_blueprint( | ||||||
|  |         "system/providers-saml.yaml", | ||||||
|  |     ) | ||||||
|  |     @reconcile_app("authentik_crypto") | ||||||
|  |     def test_sp_initiated_implicit_post(self): | ||||||
|  |         """test SAML Provider flow SP-initiated flow (implicit consent)""" | ||||||
|  |         # Bootstrap all needed objects | ||||||
|  |         authorization_flow = Flow.objects.get( | ||||||
|  |             slug="default-provider-authorization-implicit-consent" | ||||||
|  |         ) | ||||||
|  |         provider: SAMLProvider = SAMLProvider.objects.create( | ||||||
|  |             name="saml-test", | ||||||
|  |             acs_url="http://localhost:9009/saml/acs", | ||||||
|  |             audience="authentik-e2e", | ||||||
|  |             issuer="authentik-e2e", | ||||||
|  |             sp_binding=SAMLBindings.POST, | ||||||
|  |             authorization_flow=authorization_flow, | ||||||
|  |             signing_kp=create_test_cert(), | ||||||
|  |         ) | ||||||
|  |         provider.property_mappings.set(SAMLPropertyMapping.objects.all()) | ||||||
|  |         provider.save() | ||||||
|  |         Application.objects.create( | ||||||
|  |             name="SAML", | ||||||
|  |             slug="authentik-saml", | ||||||
|  |             provider=provider, | ||||||
|  |         ) | ||||||
|  |         self.setup_client(provider, True) | ||||||
|  |         self.driver.get("http://localhost:9009") | ||||||
|  |         self.login() | ||||||
|  |         self.wait_for_url("http://localhost:9009/") | ||||||
|  |  | ||||||
|  |         body = loads(self.driver.find_element(By.CSS_SELECTOR, "pre").text) | ||||||
|  |  | ||||||
|  |         self.assertEqual( | ||||||
|  |             body["attr"]["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"], | ||||||
|  |             [self.user.name], | ||||||
|  |         ) | ||||||
|  |         self.assertEqual( | ||||||
|  |             body["attr"][ | ||||||
|  |                 "http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname" | ||||||
|  |             ], | ||||||
|  |             [self.user.username], | ||||||
|  |         ) | ||||||
|  |         self.assertEqual( | ||||||
|  |             body["attr"]["http://schemas.goauthentik.io/2021/02/saml/username"], | ||||||
|  |             [self.user.username], | ||||||
|  |         ) | ||||||
|  |         self.assertEqual( | ||||||
|  |             body["attr"]["http://schemas.goauthentik.io/2021/02/saml/uid"], | ||||||
|  |             [str(self.user.pk)], | ||||||
|  |         ) | ||||||
|  |         self.assertEqual( | ||||||
|  |             body["attr"]["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"], | ||||||
|  |             [self.user.email], | ||||||
|  |         ) | ||||||
|  |         self.assertEqual( | ||||||
|  |             body["attr"]["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn"], | ||||||
|  |             [self.user.email], | ||||||
|  |         ) | ||||||
|  |  | ||||||
|     @retry() |     @retry() | ||||||
|     @apply_blueprint( |     @apply_blueprint( | ||||||
|         "default/flow-default-authentication-flow.yaml", |         "default/flow-default-authentication-flow.yaml", | ||||||
| @ -450,3 +519,81 @@ class TestProviderSAML(SeleniumTestCase): | |||||||
|             lambda driver: driver.current_url.startswith(should_url), |             lambda driver: driver.current_url.startswith(should_url), | ||||||
|             f"URL {self.driver.current_url} doesn't match expected URL {should_url}", |             f"URL {self.driver.current_url} doesn't match expected URL {should_url}", | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|  |     @retry() | ||||||
|  |     @apply_blueprint( | ||||||
|  |         "default/flow-default-authentication-flow.yaml", | ||||||
|  |         "default/flow-default-invalidation-flow.yaml", | ||||||
|  |     ) | ||||||
|  |     @apply_blueprint( | ||||||
|  |         "default/flow-default-provider-authorization-implicit-consent.yaml", | ||||||
|  |     ) | ||||||
|  |     @apply_blueprint( | ||||||
|  |         "system/providers-saml.yaml", | ||||||
|  |     ) | ||||||
|  |     @reconcile_app("authentik_crypto") | ||||||
|  |     def test_sp_initiated_implicit_post_buffer(self): | ||||||
|  |         """test SAML Provider flow SP-initiated flow (implicit consent)""" | ||||||
|  |         # Bootstrap all needed objects | ||||||
|  |         authorization_flow = Flow.objects.get( | ||||||
|  |             slug="default-provider-authorization-implicit-consent" | ||||||
|  |         ) | ||||||
|  |         provider: SAMLProvider = SAMLProvider.objects.create( | ||||||
|  |             name="saml-test", | ||||||
|  |             acs_url=f"http://{self.host}:9009/saml/acs", | ||||||
|  |             audience="authentik-e2e", | ||||||
|  |             issuer="authentik-e2e", | ||||||
|  |             sp_binding=SAMLBindings.POST, | ||||||
|  |             authorization_flow=authorization_flow, | ||||||
|  |             signing_kp=create_test_cert(), | ||||||
|  |         ) | ||||||
|  |         provider.property_mappings.set(SAMLPropertyMapping.objects.all()) | ||||||
|  |         provider.save() | ||||||
|  |         Application.objects.create( | ||||||
|  |             name="SAML", | ||||||
|  |             slug="authentik-saml", | ||||||
|  |             provider=provider, | ||||||
|  |         ) | ||||||
|  |         self.setup_client(provider, True, SP_ROOT_URL=f"http://{self.host}:9009") | ||||||
|  |  | ||||||
|  |         self.driver.get(self.live_server_url) | ||||||
|  |         login_window = self.driver.current_window_handle | ||||||
|  |         self.driver.switch_to.new_window("tab") | ||||||
|  |         client_window = self.driver.current_window_handle | ||||||
|  |         # We need to access the SP on the same host as the IdP for SameSite cookies | ||||||
|  |         self.driver.get(f"http://{self.host}:9009") | ||||||
|  |  | ||||||
|  |         self.driver.switch_to.window(login_window) | ||||||
|  |         self.login() | ||||||
|  |         self.driver.switch_to.window(client_window) | ||||||
|  |  | ||||||
|  |         self.wait_for_url(f"http://{self.host}:9009/") | ||||||
|  |  | ||||||
|  |         body = loads(self.driver.find_element(By.CSS_SELECTOR, "pre").text) | ||||||
|  |  | ||||||
|  |         self.assertEqual( | ||||||
|  |             body["attr"]["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"], | ||||||
|  |             [self.user.name], | ||||||
|  |         ) | ||||||
|  |         self.assertEqual( | ||||||
|  |             body["attr"][ | ||||||
|  |                 "http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname" | ||||||
|  |             ], | ||||||
|  |             [self.user.username], | ||||||
|  |         ) | ||||||
|  |         self.assertEqual( | ||||||
|  |             body["attr"]["http://schemas.goauthentik.io/2021/02/saml/username"], | ||||||
|  |             [self.user.username], | ||||||
|  |         ) | ||||||
|  |         self.assertEqual( | ||||||
|  |             body["attr"]["http://schemas.goauthentik.io/2021/02/saml/uid"], | ||||||
|  |             [str(self.user.pk)], | ||||||
|  |         ) | ||||||
|  |         self.assertEqual( | ||||||
|  |             body["attr"]["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"], | ||||||
|  |             [self.user.email], | ||||||
|  |         ) | ||||||
|  |         self.assertEqual( | ||||||
|  |             body["attr"]["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn"], | ||||||
|  |             [self.user.email], | ||||||
|  |         ) | ||||||
|  | |||||||
							
								
								
									
										70
									
								
								uv.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										70
									
								
								uv.lock
									
									
									
										generated
									
									
									
								
							| @ -379,11 +379,11 @@ wheels = [ | |||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "automat" | name = "automat" | ||||||
| version = "25.4.16" | version = "24.8.1" | ||||||
| source = { registry = "https://pypi.org/simple" } | source = { registry = "https://pypi.org/simple" } | ||||||
| sdist = { url = "https://files.pythonhosted.org/packages/e3/0f/d40bbe294bbf004d436a8bcbcfaadca8b5140d39ad0ad3d73d1a8ba15f14/automat-25.4.16.tar.gz", hash = "sha256:0017591a5477066e90d26b0e696ddc143baafd87b588cfac8100bc6be9634de0", size = 129977 } | sdist = { url = "https://files.pythonhosted.org/packages/8d/2d/ede4ad7fc34ab4482389fa3369d304f2fa22e50770af706678f6a332fa82/automat-24.8.1.tar.gz", hash = "sha256:b34227cf63f6325b8ad2399ede780675083e439b20c323d376373d8ee6306d88", size = 128679 } | ||||||
| wheels = [ | wheels = [ | ||||||
|     { url = "https://files.pythonhosted.org/packages/02/ff/1175b0b7371e46244032d43a56862d0af455823b5280a50c63d99cc50f18/automat-25.4.16-py3-none-any.whl", hash = "sha256:04e9bce696a8d5671ee698005af6e5a9fa15354140a87f4870744604dcdd3ba1", size = 42842 }, |     { url = "https://files.pythonhosted.org/packages/af/cc/55a32a2c98022d88812b5986d2a92c4ff3ee087e83b712ebc703bba452bf/Automat-24.8.1-py3-none-any.whl", hash = "sha256:bf029a7bc3da1e2c24da2343e7598affaa9f10bf0ab63ff808566ce90551e02a", size = 42585 }, | ||||||
| ] | ] | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| @ -558,30 +558,30 @@ wheels = [ | |||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "boto3" | name = "boto3" | ||||||
| version = "1.37.35" | version = "1.37.34" | ||||||
| source = { registry = "https://pypi.org/simple" } | source = { registry = "https://pypi.org/simple" } | ||||||
| dependencies = [ | dependencies = [ | ||||||
|     { name = "botocore" }, |     { name = "botocore" }, | ||||||
|     { name = "jmespath" }, |     { name = "jmespath" }, | ||||||
|     { name = "s3transfer" }, |     { name = "s3transfer" }, | ||||||
| ] | ] | ||||||
| sdist = { url = "https://files.pythonhosted.org/packages/48/5f/e356ecd2f236e6ddc7711eaf3f075c15b13e2d044cfdb47719d49c4ae7dd/boto3-1.37.35.tar.gz", hash = "sha256:751ed599c8fd9ca24896edcd6620e8a32b3db1b68efea3a90126312240e668a2", size = 111640 } | sdist = { url = "https://files.pythonhosted.org/packages/39/5d/6b1ca20ba4da350799509a69f2d295ae11d5ec08a98e82f74b5708a8180c/boto3-1.37.34.tar.gz", hash = "sha256:94ca07328474db3fa605eb99b011512caa73f7161740d365a1f00cfebfb6dd90", size = 111701 } | ||||||
| wheels = [ | wheels = [ | ||||||
|     { url = "https://files.pythonhosted.org/packages/f6/e4/00958f65ac74ab0a76af33f16c8fdf5726a5c6f0d3c0d0c058ff0dd00fd7/boto3-1.37.35-py3-none-any.whl", hash = "sha256:5a90d674830adbaf86456d6b27a18f5f11378277da5286511fa860d2e7b14261", size = 139922 }, |     { url = "https://files.pythonhosted.org/packages/cb/2e/ad43d1e87d46d11dcf4104f97b9a7f6beb38a52a0e752edfadf3eb8b6e38/boto3-1.37.34-py3-none-any.whl", hash = "sha256:586bfa72a00601c04067f9adcbb08ecaf63b05b7d731103f33cb2ce0d6950b1b", size = 139920 }, | ||||||
| ] | ] | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "botocore" | name = "botocore" | ||||||
| version = "1.37.35" | version = "1.37.34" | ||||||
| source = { registry = "https://pypi.org/simple" } | source = { registry = "https://pypi.org/simple" } | ||||||
| dependencies = [ | dependencies = [ | ||||||
|     { name = "jmespath" }, |     { name = "jmespath" }, | ||||||
|     { name = "python-dateutil" }, |     { name = "python-dateutil" }, | ||||||
|     { name = "urllib3" }, |     { name = "urllib3" }, | ||||||
| ] | ] | ||||||
| sdist = { url = "https://files.pythonhosted.org/packages/64/0b/d281d74d53f7d4733402aed7a536275084fa344a2672f7ea4dbc8ebe1f1b/botocore-1.37.35.tar.gz", hash = "sha256:197a9bf8251c45b9d882c405ec0d0ab40c10e2d2a55ee66960185daec4beb6ec", size = 13821053 } | sdist = { url = "https://files.pythonhosted.org/packages/ca/60/9ec251a0e2d3994f3eac8bd9741576757c3aad189abbdec8fab6011f5a1a/botocore-1.37.34.tar.gz", hash = "sha256:2909b6dbf9c90347c71a6fa0364acee522d6a7664f13d6f7996c9dd1b1f46fac", size = 13817141 } | ||||||
| wheels = [ | wheels = [ | ||||||
|     { url = "https://files.pythonhosted.org/packages/22/00/bf9c894f5af8e35b06ecf757d4a95883408e71c48642dc7f8760580584fd/botocore-1.37.35-py3-none-any.whl", hash = "sha256:50839212e90650d0b0fa6b8f7514876bf802f6164f2775f3abcd4d53c98bb73c", size = 13485892 }, |     { url = "https://files.pythonhosted.org/packages/e8/51/19fff717cc5000708c4ce3d081bb0e63ca117c6823975b33101d52fdd9f5/botocore-1.37.34-py3-none-any.whl", hash = "sha256:bd9af0db1097befd2028ba8525e32cacc04f26ccb9dbd5d48d6ecd05bc16c27a", size = 13483679 }, | ||||||
| ] | ] | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| @ -1726,16 +1726,16 @@ wheels = [ | |||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "kombu" | name = "kombu" | ||||||
| version = "5.5.3" | version = "5.5.2" | ||||||
| source = { registry = "https://pypi.org/simple" } | source = { registry = "https://pypi.org/simple" } | ||||||
| dependencies = [ | dependencies = [ | ||||||
|     { name = "amqp" }, |     { name = "amqp" }, | ||||||
|     { name = "tzdata" }, |     { name = "tzdata" }, | ||||||
|     { name = "vine" }, |     { name = "vine" }, | ||||||
| ] | ] | ||||||
| sdist = { url = "https://files.pythonhosted.org/packages/60/0a/128b65651ed8120460fc5af754241ad595eac74993115ec0de4f2d7bc459/kombu-5.5.3.tar.gz", hash = "sha256:021a0e11fcfcd9b0260ef1fb64088c0e92beb976eb59c1dfca7ddd4ad4562ea2", size = 461784 } | sdist = { url = "https://files.pythonhosted.org/packages/c8/12/7a340f48920f30d6febb65d0c4aca70ed01b29e116131152977df78a9a39/kombu-5.5.2.tar.gz", hash = "sha256:2dd27ec84fd843a4e0a7187424313f87514b344812cb98c25daddafbb6a7ff0e", size = 461522 } | ||||||
| wheels = [ | wheels = [ | ||||||
|     { url = "https://files.pythonhosted.org/packages/5d/35/1407fb0b2f5b07b50cbaf97fce09ad87d3bfefbf64f7171a8651cd8d2f68/kombu-5.5.3-py3-none-any.whl", hash = "sha256:5b0dbceb4edee50aa464f59469d34b97864be09111338cfb224a10b6a163909b", size = 209921 }, |     { url = "https://files.pythonhosted.org/packages/af/ba/939f3db0fca87715c883e42cc93045347d61a9d519c270a38e54a06db6e1/kombu-5.5.2-py3-none-any.whl", hash = "sha256:40f3674ed19603b8a771b6c74de126dbf8879755a0337caac6602faa82d539cd", size = 209763 }, | ||||||
| ] | ] | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| @ -3185,15 +3185,15 @@ socks = [ | |||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "uvicorn" | name = "uvicorn" | ||||||
| version = "0.34.2" | version = "0.34.1" | ||||||
| source = { registry = "https://pypi.org/simple" } | source = { registry = "https://pypi.org/simple" } | ||||||
| dependencies = [ | dependencies = [ | ||||||
|     { name = "click" }, |     { name = "click" }, | ||||||
|     { name = "h11" }, |     { name = "h11" }, | ||||||
| ] | ] | ||||||
| sdist = { url = "https://files.pythonhosted.org/packages/a6/ae/9bbb19b9e1c450cf9ecaef06463e40234d98d95bf572fab11b4f19ae5ded/uvicorn-0.34.2.tar.gz", hash = "sha256:0e929828f6186353a80b58ea719861d2629d766293b6d19baf086ba31d4f3328", size = 76815 } | sdist = { url = "https://files.pythonhosted.org/packages/86/37/dd92f1f9cedb5eaf74d9999044306e06abe65344ff197864175dbbd91871/uvicorn-0.34.1.tar.gz", hash = "sha256:af981725fc4b7ffc5cb3b0e9eda6258a90c4b52cb2a83ce567ae0a7ae1757afc", size = 76755 } | ||||||
| wheels = [ | wheels = [ | ||||||
|     { url = "https://files.pythonhosted.org/packages/b1/4b/4cef6ce21a2aaca9d852a6e84ef4f135d99fcd74fa75105e2fc0c8308acd/uvicorn-0.34.2-py3-none-any.whl", hash = "sha256:deb49af569084536d269fe0a6d67e3754f104cf03aba7c11c40f01aadf33c403", size = 62483 }, |     { url = "https://files.pythonhosted.org/packages/5f/38/a5801450940a858c102a7ad9e6150146a25406a119851c993148d56ab041/uvicorn-0.34.1-py3-none-any.whl", hash = "sha256:984c3a8c7ca18ebaad15995ee7401179212c59521e67bfc390c07fa2b8d2e065", size = 62404 }, | ||||||
| ] | ] | ||||||
|  |  | ||||||
| [package.optional-dependencies] | [package.optional-dependencies] | ||||||
| @ -3382,33 +3382,33 @@ wheels = [ | |||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "yarl" | name = "yarl" | ||||||
| version = "1.20.0" | version = "1.19.0" | ||||||
| source = { registry = "https://pypi.org/simple" } | source = { registry = "https://pypi.org/simple" } | ||||||
| dependencies = [ | dependencies = [ | ||||||
|     { name = "idna" }, |     { name = "idna" }, | ||||||
|     { name = "multidict" }, |     { name = "multidict" }, | ||||||
|     { name = "propcache" }, |     { name = "propcache" }, | ||||||
| ] | ] | ||||||
| sdist = { url = "https://files.pythonhosted.org/packages/62/51/c0edba5219027f6eab262e139f73e2417b0f4efffa23bf562f6e18f76ca5/yarl-1.20.0.tar.gz", hash = "sha256:686d51e51ee5dfe62dec86e4866ee0e9ed66df700d55c828a615640adc885307", size = 185258 } | sdist = { url = "https://files.pythonhosted.org/packages/fc/4d/8a8f57caccce49573e567744926f88c6ab3ca0b47a257806d1cf88584c5f/yarl-1.19.0.tar.gz", hash = "sha256:01e02bb80ae0dbed44273c304095295106e1d9470460e773268a27d11e594892", size = 184396 } | ||||||
| wheels = [ | wheels = [ | ||||||
|     { url = "https://files.pythonhosted.org/packages/c3/e8/3efdcb83073df978bb5b1a9cc0360ce596680e6c3fac01f2a994ccbb8939/yarl-1.20.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e06b9f6cdd772f9b665e5ba8161968e11e403774114420737f7884b5bd7bdf6f", size = 147089 }, |     { url = "https://files.pythonhosted.org/packages/b8/70/44ef8f69d61cb5123167a4dda87f6c739a833fbdb2ed52960b4e8409d65c/yarl-1.19.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:7b687c334da3ff8eab848c9620c47a253d005e78335e9ce0d6868ed7e8fd170b", size = 146855 }, | ||||||
|     { url = "https://files.pythonhosted.org/packages/60/c3/9e776e98ea350f76f94dd80b408eaa54e5092643dbf65fd9babcffb60509/yarl-1.20.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b9ae2fbe54d859b3ade40290f60fe40e7f969d83d482e84d2c31b9bff03e359e", size = 97706 }, |     { url = "https://files.pythonhosted.org/packages/c3/94/38c14d6c8217cc818647689f2dd647b976ced8fea08d0ac84e3c8168252b/yarl-1.19.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b0fe766febcf523a2930b819c87bb92407ae1368662c1bc267234e79b20ff894", size = 97523 }, | ||||||
|     { url = "https://files.pythonhosted.org/packages/0c/5b/45cdfb64a3b855ce074ae607b9fc40bc82e7613b94e7612b030255c93a09/yarl-1.20.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6d12b8945250d80c67688602c891237994d203d42427cb14e36d1a732eda480e", size = 95719 }, |     { url = "https://files.pythonhosted.org/packages/35/a5/43a613586a6255105c4655a911c307ef3420e49e540d6ae2c5829863fb25/yarl-1.19.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:742ceffd3c7beeb2b20d47cdb92c513eef83c9ef88c46829f88d5b06be6734ee", size = 95540 }, | ||||||
|     { url = "https://files.pythonhosted.org/packages/2d/4e/929633b249611eeed04e2f861a14ed001acca3ef9ec2a984a757b1515889/yarl-1.20.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:087e9731884621b162a3e06dc0d2d626e1542a617f65ba7cc7aeab279d55ad33", size = 343972 }, |     { url = "https://files.pythonhosted.org/packages/d4/60/ed26049f4a8b06ebfa6d5f3cb6a51b152fd57081aa818b6497474f65a631/yarl-1.19.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2af682a1e97437382ee0791eacbf540318bd487a942e068e7e0a6c571fadbbd3", size = 344386 }, | ||||||
|     { url = "https://files.pythonhosted.org/packages/49/fd/047535d326c913f1a90407a3baf7ff535b10098611eaef2c527e32e81ca1/yarl-1.20.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:69df35468b66c1a6e6556248e6443ef0ec5f11a7a4428cf1f6281f1879220f58", size = 339639 }, |     { url = "https://files.pythonhosted.org/packages/49/a6/b84899cab411f49af5986cfb44b514040788d81c8084f5811e6a7c0f1ce6/yarl-1.19.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:63702f1a098d0eaaea755e9c9d63172be1acb9e2d4aeb28b187092bcc9ca2d17", size = 338889 }, | ||||||
|     { url = "https://files.pythonhosted.org/packages/48/2f/11566f1176a78f4bafb0937c0072410b1b0d3640b297944a6a7a556e1d0b/yarl-1.20.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b2992fe29002fd0d4cbaea9428b09af9b8686a9024c840b8a2b8f4ea4abc16f", size = 353745 }, |     { url = "https://files.pythonhosted.org/packages/cc/ce/0704f7166a781b1f81bdd45c4f49eadbae0230ebd35b9ec7cd7769d3a6ff/yarl-1.19.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3560dcba3c71ae7382975dc1e912ee76e50b4cd7c34b454ed620d55464f11876", size = 353107 }, | ||||||
|     { url = "https://files.pythonhosted.org/packages/26/17/07dfcf034d6ae8837b33988be66045dd52f878dfb1c4e8f80a7343f677be/yarl-1.20.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4c903e0b42aab48abfbac668b5a9d7b6938e721a6341751331bcd7553de2dcae", size = 354178 }, |     { url = "https://files.pythonhosted.org/packages/75/e5/0ecd6f2a9cc4264c16d8dfb0d3d71ba8d03cb58f3bcd42b1df4358331189/yarl-1.19.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:68972df6a0cc47c8abaf77525a76ee5c5f6ea9bbdb79b9565b3234ded3c5e675", size = 353128 }, | ||||||
|     { url = "https://files.pythonhosted.org/packages/15/45/212604d3142d84b4065d5f8cab6582ed3d78e4cc250568ef2a36fe1cf0a5/yarl-1.20.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf099e2432131093cc611623e0b0bcc399b8cddd9a91eded8bfb50402ec35018", size = 349219 }, |     { url = "https://files.pythonhosted.org/packages/ad/c7/cd0fd1de581f1c2e8f996e704c9fd979e00106f18eebd91b0173cf1a13c6/yarl-1.19.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5684e7ff93ea74e47542232bd132f608df4d449f8968fde6b05aaf9e08a140f9", size = 349107 }, | ||||||
|     { url = "https://files.pythonhosted.org/packages/e6/e0/a10b30f294111c5f1c682461e9459935c17d467a760c21e1f7db400ff499/yarl-1.20.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a7f62f5dc70a6c763bec9ebf922be52aa22863d9496a9a30124d65b489ea672", size = 337266 }, |     { url = "https://files.pythonhosted.org/packages/e6/34/ba3e5a20bd1d6a09034fc7985aaf1309976f2a7a5aefd093c9e56f6e1e0c/yarl-1.19.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8182ad422bfacdebd4759ce3adc6055c0c79d4740aea1104e05652a81cd868c6", size = 335144 }, | ||||||
|     { url = "https://files.pythonhosted.org/packages/33/a6/6efa1d85a675d25a46a167f9f3e80104cde317dfdf7f53f112ae6b16a60a/yarl-1.20.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:54ac15a8b60382b2bcefd9a289ee26dc0920cf59b05368c9b2b72450751c6eb8", size = 360873 }, |     { url = "https://files.pythonhosted.org/packages/1e/98/d9b7beb932fade015906efe0980aa7d522b8f93cf5ebf1082e74faa314b7/yarl-1.19.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:aee5b90a5a9b71ac57400a7bdd0feaa27c51e8f961decc8d412e720a004a1791", size = 360795 }, | ||||||
|     { url = "https://files.pythonhosted.org/packages/77/67/c8ab718cb98dfa2ae9ba0f97bf3cbb7d45d37f13fe1fbad25ac92940954e/yarl-1.20.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:25b3bc0763a7aca16a0f1b5e8ef0f23829df11fb539a1b70476dcab28bd83da7", size = 360524 }, |     { url = "https://files.pythonhosted.org/packages/9a/11/70b8770039cc54af5948970591517a1e1d093df3f04f328c655c9a0fefb7/yarl-1.19.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:8c0b2371858d5a814b08542d5d548adb03ff2d7ab32f23160e54e92250961a72", size = 360140 }, | ||||||
|     { url = "https://files.pythonhosted.org/packages/bd/e8/c3f18660cea1bc73d9f8a2b3ef423def8dadbbae6c4afabdb920b73e0ead/yarl-1.20.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b2586e36dc070fc8fad6270f93242124df68b379c3a251af534030a4a33ef594", size = 365370 }, |     { url = "https://files.pythonhosted.org/packages/d4/67/708e3e36fafc4d9d96b4eecc6c8b9f37c8ad50df8a16c7a1d5ba9df53050/yarl-1.19.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cd430c2b7df4ae92498da09e9b12cad5bdbb140d22d138f9e507de1aa3edfea3", size = 364431 }, | ||||||
|     { url = "https://files.pythonhosted.org/packages/c9/99/33f3b97b065e62ff2d52817155a89cfa030a1a9b43fee7843ef560ad9603/yarl-1.20.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:866349da9d8c5290cfefb7fcc47721e94de3f315433613e01b435473be63daa6", size = 373297 }, |     { url = "https://files.pythonhosted.org/packages/c3/8b/937fbbcc895553a7e16fcd86ae4e0724c6ac9468237ad8e7c29cc3b1c9d9/yarl-1.19.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a93208282c0ccdf73065fd76c6c129bd428dba5ff65d338ae7d2ab27169861a0", size = 373832 }, | ||||||
|     { url = "https://files.pythonhosted.org/packages/3d/89/7519e79e264a5f08653d2446b26d4724b01198a93a74d2e259291d538ab1/yarl-1.20.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:33bb660b390a0554d41f8ebec5cd4475502d84104b27e9b42f5321c5192bfcd1", size = 378771 }, |     { url = "https://files.pythonhosted.org/packages/f8/ca/288ddc2230c9b6647fe907504f1119adb41252ac533eb564d3fc73511215/yarl-1.19.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:b8179280cdeb4c36eb18d6534a328f9d40da60d2b96ac4a295c5f93e2799e9d9", size = 378122 }, | ||||||
|     { url = "https://files.pythonhosted.org/packages/3a/58/6c460bbb884abd2917c3eef6f663a4a873f8dc6f498561fc0ad92231c113/yarl-1.20.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:737e9f171e5a07031cbee5e9180f6ce21a6c599b9d4b2c24d35df20a52fabf4b", size = 375000 }, |     { url = "https://files.pythonhosted.org/packages/4f/5a/79e1ef31d14968fbfc0ecec70a6683b574890d9c7550c376dd6d40de7754/yarl-1.19.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:eda3c2b42dc0c389b7cfda2c4df81c12eeb552019e0de28bde8f913fc3d1fcf3", size = 375178 }, | ||||||
|     { url = "https://files.pythonhosted.org/packages/3b/2a/dd7ed1aa23fea996834278d7ff178f215b24324ee527df53d45e34d21d28/yarl-1.20.0-cp312-cp312-win32.whl", hash = "sha256:839de4c574169b6598d47ad61534e6981979ca2c820ccb77bf70f4311dd2cc64", size = 86355 }, |     { url = "https://files.pythonhosted.org/packages/95/38/9b0e56bf14026c3f550ad6425679f6d1a2f4821d70767f39d6f4c56a0820/yarl-1.19.0-cp312-cp312-win32.whl", hash = "sha256:57f3fed859af367b9ca316ecc05ce79ce327d6466342734305aa5cc380e4d8be", size = 86172 }, | ||||||
|     { url = "https://files.pythonhosted.org/packages/ca/c6/333fe0338305c0ac1c16d5aa7cc4841208d3252bbe62172e0051006b5445/yarl-1.20.0-cp312-cp312-win_amd64.whl", hash = "sha256:3d7dbbe44b443b0c4aa0971cb07dcb2c2060e4a9bf8d1301140a33a93c98e18c", size = 92904 }, |     { url = "https://files.pythonhosted.org/packages/b3/96/5c2f3987c4bb4e5cdebea3caf99a45946b13a9516f849c02222203d99860/yarl-1.19.0-cp312-cp312-win_amd64.whl", hash = "sha256:5507c1f7dd3d41251b67eecba331c8b2157cfd324849879bebf74676ce76aff7", size = 92617 }, | ||||||
|     { url = "https://files.pythonhosted.org/packages/ea/1f/70c57b3d7278e94ed22d85e09685d3f0a38ebdd8c5c73b65ba4c0d0fe002/yarl-1.20.0-py3-none-any.whl", hash = "sha256:5d0fe6af927a47a230f31e6004621fd0959eaa915fc62acfafa67ff7229a3124", size = 46124 }, |     { url = "https://files.pythonhosted.org/packages/a4/06/ae25a353e8f032322df6f30d6bb1fc329773ee48e1a80a2196ccb8d1206b/yarl-1.19.0-py3-none-any.whl", hash = "sha256:a727101eb27f66727576630d02985d8a065d09cd0b5fcbe38a5793f71b2a97ef", size = 45990 }, | ||||||
| ] | ] | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
|  | |||||||
							
								
								
									
										23
									
								
								web/.prettierrc.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								web/.prettierrc.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,23 @@ | |||||||
|  | { | ||||||
|  |     "arrowParens": "always", | ||||||
|  |     "bracketSpacing": true, | ||||||
|  |     "embeddedLanguageFormatting": "auto", | ||||||
|  |     "htmlWhitespaceSensitivity": "css", | ||||||
|  |     "insertPragma": false, | ||||||
|  |     "jsxSingleQuote": false, | ||||||
|  |     "printWidth": 100, | ||||||
|  |     "proseWrap": "preserve", | ||||||
|  |     "quoteProps": "consistent", | ||||||
|  |     "requirePragma": false, | ||||||
|  |     "semi": true, | ||||||
|  |     "singleQuote": false, | ||||||
|  |     "tabWidth": 4, | ||||||
|  |     "trailingComma": "all", | ||||||
|  |     "useTabs": false, | ||||||
|  |     "vueIndentScriptAndStyle": false, | ||||||
|  |     "plugins": ["@trivago/prettier-plugin-sort-imports"], | ||||||
|  |     "importOrder": ["^(@?)lit(.*)$", "\\.css$", "^@goauthentik/api$", "^[./]"], | ||||||
|  |     "importOrderSeparation": true, | ||||||
|  |     "importOrderSortSpecifiers": true, | ||||||
|  |     "importOrderParserPlugins": ["typescript", "jsx", "classProperties", "decorators-legacy"] | ||||||
|  | } | ||||||
							
								
								
									
										854
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										854
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -12,7 +12,8 @@ | |||||||
|         "@floating-ui/dom": "^1.6.11", |         "@floating-ui/dom": "^1.6.11", | ||||||
|         "@formatjs/intl-listformat": "^7.5.7", |         "@formatjs/intl-listformat": "^7.5.7", | ||||||
|         "@fortawesome/fontawesome-free": "^6.6.0", |         "@fortawesome/fontawesome-free": "^6.6.0", | ||||||
|         "@goauthentik/api": "^2025.2.4-1745325566", |         "@goauthentik/api": "^2025.2.4-1744640358", | ||||||
|  |         "@lit-labs/ssr": "^3.2.2", | ||||||
|         "@lit/context": "^1.1.2", |         "@lit/context": "^1.1.2", | ||||||
|         "@lit/localize": "^0.12.2", |         "@lit/localize": "^0.12.2", | ||||||
|         "@lit/reactive-element": "^2.0.4", |         "@lit/reactive-element": "^2.0.4", | ||||||
| @ -53,7 +54,6 @@ | |||||||
|         "remark-gfm": "^4.0.1", |         "remark-gfm": "^4.0.1", | ||||||
|         "remark-mdx-frontmatter": "^5.0.0", |         "remark-mdx-frontmatter": "^5.0.0", | ||||||
|         "style-mod": "^4.1.2", |         "style-mod": "^4.1.2", | ||||||
|         "trusted-types": "^2.0.0", |  | ||||||
|         "ts-pattern": "^5.4.0", |         "ts-pattern": "^5.4.0", | ||||||
|         "unist-util-visit": "^5.0.0", |         "unist-util-visit": "^5.0.0", | ||||||
|         "webcomponent-qr-code": "^1.2.0", |         "webcomponent-qr-code": "^1.2.0", | ||||||
| @ -61,9 +61,6 @@ | |||||||
|     }, |     }, | ||||||
|     "devDependencies": { |     "devDependencies": { | ||||||
|         "@eslint/js": "^9.11.1", |         "@eslint/js": "^9.11.1", | ||||||
|         "@goauthentik/esbuild-plugin-live-reload": "^1.0.4", |  | ||||||
|         "@goauthentik/prettier-config": "^1.0.4", |  | ||||||
|         "@goauthentik/tsconfig": "^1.0.4", |  | ||||||
|         "@hcaptcha/types": "^1.0.4", |         "@hcaptcha/types": "^1.0.4", | ||||||
|         "@lit/localize-tools": "^0.8.0", |         "@lit/localize-tools": "^0.8.0", | ||||||
|         "@rollup/plugin-replace": "^6.0.1", |         "@rollup/plugin-replace": "^6.0.1", | ||||||
| @ -75,7 +72,7 @@ | |||||||
|         "@storybook/manager-api": "^8.3.4", |         "@storybook/manager-api": "^8.3.4", | ||||||
|         "@storybook/web-components": "^8.3.4", |         "@storybook/web-components": "^8.3.4", | ||||||
|         "@storybook/web-components-vite": "^8.3.4", |         "@storybook/web-components-vite": "^8.3.4", | ||||||
|         "@trivago/prettier-plugin-sort-imports": "^5.2.2", |         "@trivago/prettier-plugin-sort-imports": "^4.3.0", | ||||||
|         "@types/chart.js": "^2.9.41", |         "@types/chart.js": "^2.9.41", | ||||||
|         "@types/codemirror": "^5.60.15", |         "@types/codemirror": "^5.60.15", | ||||||
|         "@types/dompurify": "^3.0.5", |         "@types/dompurify": "^3.0.5", | ||||||
| @ -98,6 +95,7 @@ | |||||||
|         "eslint": "^9.11.1", |         "eslint": "^9.11.1", | ||||||
|         "eslint-plugin-lit": "^1.15.0", |         "eslint-plugin-lit": "^1.15.0", | ||||||
|         "eslint-plugin-wc": "^2.1.1", |         "eslint-plugin-wc": "^2.1.1", | ||||||
|  |         "find-free-ports": "^3.1.1", | ||||||
|         "github-slugger": "^2.0.0", |         "github-slugger": "^2.0.0", | ||||||
|         "glob": "^11.0.0", |         "glob": "^11.0.0", | ||||||
|         "globals": "^15.10.0", |         "globals": "^15.10.0", | ||||||
| @ -138,7 +136,6 @@ | |||||||
|             "axios": "^1.8.4" |             "axios": "^1.8.4" | ||||||
|         } |         } | ||||||
|     }, |     }, | ||||||
|     "prettier": "@goauthentik/prettier-config", |  | ||||||
|     "private": true, |     "private": true, | ||||||
|     "scripts": { |     "scripts": { | ||||||
|         "build": "wireit", |         "build": "wireit", | ||||||
| @ -274,7 +271,7 @@ | |||||||
|             "command": "tsc --noEmit -p ./tests" |             "command": "tsc --noEmit -p ./tests" | ||||||
|         }, |         }, | ||||||
|         "lint:types": { |         "lint:types": { | ||||||
|             "command": "NODE_OPTIONS=\"--max-old-space-size=3000\" tsc -b .", |             "command": "tsc --noEmit -p .", | ||||||
|             "dependencies": [ |             "dependencies": [ | ||||||
|                 "build-locales", |                 "build-locales", | ||||||
|                 "lint:types:tests" |                 "lint:types:tests" | ||||||
|  | |||||||
| @ -1,13 +0,0 @@ | |||||||
| /// <reference types="./types.js" /> |  | ||||||
| /** |  | ||||||
|  * @file Entry point for the ESBuild client-side observer. |  | ||||||
|  */ |  | ||||||
| import { ESBuildObserver } from "./ESBuildObserver.js"; |  | ||||||
|  |  | ||||||
| if (import.meta.env?.ESBUILD_WATCHER_URL) { |  | ||||||
|     const buildObserver = new ESBuildObserver(import.meta.env.ESBUILD_WATCHER_URL); |  | ||||||
|  |  | ||||||
|     window.addEventListener("beforeunload", () => { |  | ||||||
|         buildObserver.dispose(); |  | ||||||
|     }); |  | ||||||
| } |  | ||||||
| @ -1,18 +0,0 @@ | |||||||
| /** |  | ||||||
|  * @file Import meta environment variables available via ESBuild. |  | ||||||
|  */ |  | ||||||
|  |  | ||||||
| export {}; |  | ||||||
| declare global { |  | ||||||
|     interface ImportMeta { |  | ||||||
|         readonly env: { |  | ||||||
|             /** |  | ||||||
|              * The injected watcher URL for ESBuild. |  | ||||||
|              * This is used for live reloading in development mode. |  | ||||||
|              * |  | ||||||
|              * @format url |  | ||||||
|              */ |  | ||||||
|             ESBUILD_WATCHER_URL: string; |  | ||||||
|         }; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @ -1,2 +0,0 @@ | |||||||
| export * from "./client/index.js"; |  | ||||||
| export * from "./plugin/index.js"; |  | ||||||
| @ -1,53 +0,0 @@ | |||||||
| { |  | ||||||
|     "name": "@goauthentik/esbuild-plugin-live-reload", |  | ||||||
|     "description": "ESBuild plugin to watch for file changes and trigger client-side reloads.", |  | ||||||
|     "version": "1.0.4", |  | ||||||
|     "dependencies": { |  | ||||||
|         "find-free-ports": "^3.1.1" |  | ||||||
|     }, |  | ||||||
|     "devDependencies": { |  | ||||||
|         "@goauthentik/prettier-config": "^1.0.4", |  | ||||||
|         "@goauthentik/tsconfig": "^1.0.4", |  | ||||||
|         "@trivago/prettier-plugin-sort-imports": "^5.2.2", |  | ||||||
|         "@types/node": "^22.14.1", |  | ||||||
|         "esbuild": "^0.25.0", |  | ||||||
|         "prettier": "^3.3.3", |  | ||||||
|         "typescript": "^5.6.2" |  | ||||||
|     }, |  | ||||||
|     "engines": { |  | ||||||
|         "node": ">=20.11" |  | ||||||
|     }, |  | ||||||
|     "exports": { |  | ||||||
|         "./package.json": "./package.json", |  | ||||||
|         ".": { |  | ||||||
|             "types": "./out/index.d.ts", |  | ||||||
|             "import": "./index.js" |  | ||||||
|         }, |  | ||||||
|         "./client": { |  | ||||||
|             "types": "./out/client/index.d.ts", |  | ||||||
|             "import": "./client/index.js" |  | ||||||
|         }, |  | ||||||
|         "./plugin": { |  | ||||||
|             "types": "./out/plugin/index.d.ts", |  | ||||||
|             "import": "./plugin/index.js" |  | ||||||
|         } |  | ||||||
|     }, |  | ||||||
|     "files": [ |  | ||||||
|         "./index.js", |  | ||||||
|         "client/**/*", |  | ||||||
|         "plugin/**/*", |  | ||||||
|         "out/**/*" |  | ||||||
|     ], |  | ||||||
|     "license": "MIT", |  | ||||||
|     "main": "index.js", |  | ||||||
|     "peerDependencies": { |  | ||||||
|         "esbuild": "^0.25.0" |  | ||||||
|     }, |  | ||||||
|     "prettier": "@goauthentik/prettier-config", |  | ||||||
|     "private": true, |  | ||||||
|     "publishConfig": { |  | ||||||
|         "access": "public" |  | ||||||
|     }, |  | ||||||
|     "type": "module", |  | ||||||
|     "types": "./out/index.d.ts" |  | ||||||
| } |  | ||||||
| @ -1,243 +0,0 @@ | |||||||
| /** |  | ||||||
|  * @file Live reload plugin for ESBuild. |  | ||||||
|  * |  | ||||||
|  * @import { ListenOptions } from "node:net"; |  | ||||||
|  * @import {Server as HTTPServer} from "node:http"; |  | ||||||
|  * @import {Server as HTTPSServer} from "node:https"; |  | ||||||
|  */ |  | ||||||
| import { findFreePorts } from "find-free-ports"; |  | ||||||
| import * as http from "node:http"; |  | ||||||
| import * as path from "node:path"; |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Serializes a custom event to a text stream. |  | ||||||
|  * @param {Event} event |  | ||||||
|  * @returns {string} |  | ||||||
|  */ |  | ||||||
| export function serializeCustomEventToStream(event) { |  | ||||||
|     // @ts-expect-error - TS doesn't know about the detail property |  | ||||||
|     const data = event.detail ?? {}; |  | ||||||
|  |  | ||||||
|     const eventContent = [`event: ${event.type}`, `data: ${JSON.stringify(data)}`]; |  | ||||||
|  |  | ||||||
|     return eventContent.join("\n") + "\n\n"; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| const MIN_PORT = 1025; |  | ||||||
| const MAX_PORT = 65535; |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Find a random port that is not in use, sufficiently far from the default port. |  | ||||||
|  * @returns {Promise<number>} |  | ||||||
|  */ |  | ||||||
| async function findDisparatePort() { |  | ||||||
|     const startPort = Math.floor(Math.random() * (MAX_PORT - MIN_PORT + 1)) + MIN_PORT; |  | ||||||
|  |  | ||||||
|     const wathcherPorts = await findFreePorts(1, { |  | ||||||
|         startPort, |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     const [port] = wathcherPorts; |  | ||||||
|  |  | ||||||
|     if (!port) { |  | ||||||
|         throw new Error("No free ports available"); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     return port; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Event server initialization options. |  | ||||||
|  * |  | ||||||
|  * @typedef {Object} EventServerInit |  | ||||||
|  * |  | ||||||
|  * @property {string} pathname |  | ||||||
|  * @property {EventTarget} dispatcher |  | ||||||
|  * @property {string} [logPrefix] |  | ||||||
|  */ |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * @typedef {(req: http.IncomingMessage, res: http.ServerResponse) => void} RequestHandler |  | ||||||
|  */ |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Create an event request handler. |  | ||||||
|  * @param {EventServerInit} options |  | ||||||
|  * @returns {RequestHandler} |  | ||||||
|  * @category ESBuild |  | ||||||
|  */ |  | ||||||
| export function createRequestHandler({ pathname, dispatcher, logPrefix = "Build Observer" }) { |  | ||||||
|     // eslint-disable-next-line no-console |  | ||||||
|     const log = console.log.bind(console, `[${logPrefix}]`); |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * @type {RequestHandler} |  | ||||||
|      */ |  | ||||||
|     const requestHandler = (req, res) => { |  | ||||||
|         res.setHeader("Access-Control-Allow-Origin", "*"); |  | ||||||
|         res.setHeader("Access-Control-Allow-Methods", "GET"); |  | ||||||
|         res.setHeader("Access-Control-Allow-Headers", "Content-Type"); |  | ||||||
|  |  | ||||||
|         if (req.url !== pathname) { |  | ||||||
|             log(`🚫 Invalid request to ${req.url}`); |  | ||||||
|             res.writeHead(404); |  | ||||||
|             res.end(); |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         log("🔌 Client connected"); |  | ||||||
|  |  | ||||||
|         res.writeHead(200, { |  | ||||||
|             "Content-Type": "text/event-stream", |  | ||||||
|             "Cache-Control": "no-cache", |  | ||||||
|             "Connection": "keep-alive", |  | ||||||
|         }); |  | ||||||
|  |  | ||||||
|         /** |  | ||||||
|          * @param {Event} event |  | ||||||
|          */ |  | ||||||
|         const listener = (event) => { |  | ||||||
|             const body = serializeCustomEventToStream(event); |  | ||||||
|  |  | ||||||
|             res.write(body); |  | ||||||
|         }; |  | ||||||
|  |  | ||||||
|         dispatcher.addEventListener("esbuild:start", listener); |  | ||||||
|         dispatcher.addEventListener("esbuild:error", listener); |  | ||||||
|         dispatcher.addEventListener("esbuild:end", listener); |  | ||||||
|  |  | ||||||
|         req.on("close", () => { |  | ||||||
|             log("🔌 Client disconnected"); |  | ||||||
|  |  | ||||||
|             clearInterval(keepAliveInterval); |  | ||||||
|  |  | ||||||
|             dispatcher.removeEventListener("esbuild:start", listener); |  | ||||||
|             dispatcher.removeEventListener("esbuild:error", listener); |  | ||||||
|             dispatcher.removeEventListener("esbuild:end", listener); |  | ||||||
|         }); |  | ||||||
|  |  | ||||||
|         const keepAliveInterval = setInterval(() => { |  | ||||||
|             console.timeStamp("🏓 Keep-alive"); |  | ||||||
|  |  | ||||||
|             res.write("event: keep-alive\n\n"); |  | ||||||
|             res.write(serializeCustomEventToStream(new CustomEvent("esbuild:keep-alive"))); |  | ||||||
|         }, 15_000); |  | ||||||
|     }; |  | ||||||
|  |  | ||||||
|     return requestHandler; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Options for the build observer plugin. |  | ||||||
|  * |  | ||||||
|  * @typedef {object} BuildObserverOptions |  | ||||||
|  * |  | ||||||
|  * @property {HTTPServer | HTTPSServer} [server] |  | ||||||
|  * @property {ListenOptions} [listenOptions] |  | ||||||
|  * @property {string | URL} [publicURL] |  | ||||||
|  * @property {string} [logPrefix] |  | ||||||
|  * @property {string} [relativeRoot] |  | ||||||
|  */ |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Creates a plugin that listens for build events and sends them to a server-sent event stream. |  | ||||||
|  * |  | ||||||
|  * @param {BuildObserverOptions} [options] |  | ||||||
|  * @returns {import('esbuild').Plugin} |  | ||||||
|  */ |  | ||||||
| export function liveReloadPlugin(options = {}) { |  | ||||||
|     return { |  | ||||||
|         name: "build-watcher", |  | ||||||
|         setup: async (build) => { |  | ||||||
|             const logPrefix = options.logPrefix || "Build Observer"; |  | ||||||
|  |  | ||||||
|             const timerLabel = `[${logPrefix}] 🏁`; |  | ||||||
|             const relativeRoot = options.relativeRoot || process.cwd(); |  | ||||||
|  |  | ||||||
|             const dispatcher = new EventTarget(); |  | ||||||
|  |  | ||||||
|             /** |  | ||||||
|              * @type {URL} |  | ||||||
|              */ |  | ||||||
|             let publicURL; |  | ||||||
|  |  | ||||||
|             if (!options.publicURL) { |  | ||||||
|                 const port = await findDisparatePort(); |  | ||||||
|  |  | ||||||
|                 publicURL = new URL(`http://localhost:${port}/events`); |  | ||||||
|             } else { |  | ||||||
|                 publicURL = |  | ||||||
|                     typeof options.publicURL === "string" |  | ||||||
|                         ? new URL(options.publicURL) |  | ||||||
|                         : options.publicURL; |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             build.initialOptions.define = { |  | ||||||
|                 ...build.initialOptions.define, |  | ||||||
|                 "import.meta.env.ESBUILD_WATCHER_URL": JSON.stringify(publicURL.href), |  | ||||||
|             }; |  | ||||||
|  |  | ||||||
|             const requestHandler = createRequestHandler({ |  | ||||||
|                 pathname: publicURL.pathname, |  | ||||||
|                 dispatcher, |  | ||||||
|                 logPrefix, |  | ||||||
|             }); |  | ||||||
|  |  | ||||||
|             const server = options.server || http.createServer(requestHandler); |  | ||||||
|  |  | ||||||
|             const listenOptions = options.listenOptions || { |  | ||||||
|                 port: parseInt(publicURL.port, 10), |  | ||||||
|                 host: publicURL.hostname, |  | ||||||
|             }; |  | ||||||
|  |  | ||||||
|             server.listen(listenOptions, () => { |  | ||||||
|                 // eslint-disable-next-line no-console |  | ||||||
|                 console.log(`[${logPrefix}] Listening`); |  | ||||||
|             }); |  | ||||||
|  |  | ||||||
|             build.onDispose(() => { |  | ||||||
|                 server?.close(); |  | ||||||
|             }); |  | ||||||
|  |  | ||||||
|             build.onStart(() => { |  | ||||||
|                 console.time(timerLabel); |  | ||||||
|  |  | ||||||
|                 dispatcher.dispatchEvent( |  | ||||||
|                     new CustomEvent("esbuild:start", { |  | ||||||
|                         detail: new Date().toISOString(), |  | ||||||
|                     }), |  | ||||||
|                 ); |  | ||||||
|             }); |  | ||||||
|  |  | ||||||
|             build.onEnd((buildResult) => { |  | ||||||
|                 console.timeEnd(timerLabel); |  | ||||||
|  |  | ||||||
|                 if (!buildResult.errors.length) { |  | ||||||
|                     dispatcher.dispatchEvent( |  | ||||||
|                         new CustomEvent("esbuild:end", { |  | ||||||
|                             detail: new Date().toISOString(), |  | ||||||
|                         }), |  | ||||||
|                     ); |  | ||||||
|  |  | ||||||
|                     return; |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 console.warn(`Build ended with ${buildResult.errors.length} errors`); |  | ||||||
|  |  | ||||||
|                 dispatcher.dispatchEvent( |  | ||||||
|                     new CustomEvent("esbuild:error", { |  | ||||||
|                         detail: buildResult.errors.map((error) => ({ |  | ||||||
|                             ...error, |  | ||||||
|                             location: error.location |  | ||||||
|                                 ? { |  | ||||||
|                                       ...error.location, |  | ||||||
|                                       file: path.resolve(relativeRoot, error.location.file), |  | ||||||
|                                   } |  | ||||||
|                                 : null, |  | ||||||
|                         })), |  | ||||||
|                     }), |  | ||||||
|                 ); |  | ||||||
|             }); |  | ||||||
|         }, |  | ||||||
|     }; |  | ||||||
| } |  | ||||||
| @ -1,10 +0,0 @@ | |||||||
| { |  | ||||||
|     "extends": "@goauthentik/tsconfig", |  | ||||||
|     "compilerOptions": { |  | ||||||
|         "lib": ["ESNext", "DOM", "DOM.Iterable"], |  | ||||||
|         "resolveJsonModule": true, |  | ||||||
|         "baseUrl": ".", |  | ||||||
|         "checkJs": true, |  | ||||||
|         "emitDeclarationOnly": true |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @ -1,13 +1,8 @@ | |||||||
| /** |  | ||||||
|  * @file ESBuild script for building the authentik web UI. |  | ||||||
|  * |  | ||||||
|  * @import { BuildOptions } from "esbuild"; |  | ||||||
|  */ |  | ||||||
| import { liveReloadPlugin } from "@goauthentik/esbuild-plugin-live-reload/plugin"; |  | ||||||
| import { execFileSync } from "child_process"; | import { execFileSync } from "child_process"; | ||||||
| import { deepmerge } from "deepmerge-ts"; | import { deepmerge } from "deepmerge-ts"; | ||||||
| import esbuild from "esbuild"; | import esbuild from "esbuild"; | ||||||
| import { polyfillNode } from "esbuild-plugin-polyfill-node"; | import { polyfillNode } from "esbuild-plugin-polyfill-node"; | ||||||
|  | import findFreePorts from "find-free-ports"; | ||||||
| import { copyFileSync, mkdirSync, readFileSync, statSync } from "fs"; | import { copyFileSync, mkdirSync, readFileSync, statSync } from "fs"; | ||||||
| import { globSync } from "glob"; | import { globSync } from "glob"; | ||||||
| import * as path from "path"; | import * as path from "path"; | ||||||
| @ -16,6 +11,7 @@ import process from "process"; | |||||||
| import { fileURLToPath } from "url"; | import { fileURLToPath } from "url"; | ||||||
|  |  | ||||||
| import { mdxPlugin } from "./esbuild/build-mdx-plugin.mjs"; | import { mdxPlugin } from "./esbuild/build-mdx-plugin.mjs"; | ||||||
|  | import { buildObserverPlugin } from "./esbuild/build-observer-plugin.mjs"; | ||||||
|  |  | ||||||
| const __dirname = fileURLToPath(new URL(".", import.meta.url)); | const __dirname = fileURLToPath(new URL(".", import.meta.url)); | ||||||
| let authentikProjectRoot = path.join(__dirname, "..", ".."); | let authentikProjectRoot = path.join(__dirname, "..", ".."); | ||||||
| @ -124,7 +120,7 @@ const BASE_ESBUILD_OPTIONS = { | |||||||
|     splitting: true, |     splitting: true, | ||||||
|     treeShaking: true, |     treeShaking: true, | ||||||
|     external: ["*.woff", "*.woff2"], |     external: ["*.woff", "*.woff2"], | ||||||
|     tsconfig: path.resolve(__dirname, "..", "tsconfig.build.json"), |     tsconfig: "./tsconfig.json", | ||||||
|     loader: { |     loader: { | ||||||
|         ".css": "text", |         ".css": "text", | ||||||
|     }, |     }, | ||||||
| @ -224,17 +220,26 @@ function doHelp() { | |||||||
| async function doWatch() { | async function doWatch() { | ||||||
|     console.log("Watching all entry points..."); |     console.log("Watching all entry points..."); | ||||||
|  |  | ||||||
|  |     const wathcherPorts = await findFreePorts(entryPoints.length); | ||||||
|  |  | ||||||
|     const buildContexts = await Promise.all( |     const buildContexts = await Promise.all( | ||||||
|         entryPoints.map((entryPoint) => { |         entryPoints.map((entryPoint, i) => { | ||||||
|  |             const port = wathcherPorts[i]; | ||||||
|  |             const serverURL = new URL(`http://localhost:${port}/events`); | ||||||
|  |  | ||||||
|             return esbuild.context( |             return esbuild.context( | ||||||
|                 createEntryPointOptions(entryPoint, { |                 createEntryPointOptions(entryPoint, { | ||||||
|                     define: definitions, |  | ||||||
|                     plugins: [ |                     plugins: [ | ||||||
|                         liveReloadPlugin({ |                         buildObserverPlugin({ | ||||||
|                             logPrefix: `Build Observer (${entryPoint[1]})`, |                             serverURL, | ||||||
|  |                             logPrefix: entryPoint[1], | ||||||
|                             relativeRoot: path.join(__dirname, ".."), |                             relativeRoot: path.join(__dirname, ".."), | ||||||
|                         }), |                         }), | ||||||
|                     ], |                     ], | ||||||
|  |                     define: { | ||||||
|  |                         ...definitions, | ||||||
|  |                         "process.env.WATCHER_URL": JSON.stringify(serverURL.toString()), | ||||||
|  |                     }, | ||||||
|                 }), |                 }), | ||||||
|             ); |             ); | ||||||
|         }), |         }), | ||||||
|  | |||||||
							
								
								
									
										141
									
								
								web/scripts/esbuild/build-observer-plugin.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										141
									
								
								web/scripts/esbuild/build-observer-plugin.mjs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,141 @@ | |||||||
|  | import * as http from "http"; | ||||||
|  | import path from "path"; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Serializes a custom event to a text stream. | ||||||
|  |  * a | ||||||
|  |  * @param {Event} event | ||||||
|  |  * @returns {string} | ||||||
|  |  */ | ||||||
|  | export function serializeCustomEventToStream(event) { | ||||||
|  |     // @ts-expect-error - TS doesn't know about the detail property | ||||||
|  |     const data = event.detail ?? {}; | ||||||
|  |  | ||||||
|  |     const eventContent = [`event: ${event.type}`, `data: ${JSON.stringify(data)}`]; | ||||||
|  |  | ||||||
|  |     return eventContent.join("\n") + "\n\n"; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Options for the build observer plugin. | ||||||
|  |  * | ||||||
|  |  * @typedef {Object} BuildObserverOptions | ||||||
|  |  * | ||||||
|  |  * @property {URL} serverURL | ||||||
|  |  * @property {string} logPrefix | ||||||
|  |  * @property {string} relativeRoot | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Creates a plugin that listens for build events and sends them to a server-sent event stream. | ||||||
|  |  * | ||||||
|  |  * @param {BuildObserverOptions} options | ||||||
|  |  * @returns {import('esbuild').Plugin} | ||||||
|  |  */ | ||||||
|  | export function buildObserverPlugin({ serverURL, logPrefix, relativeRoot }) { | ||||||
|  |     const timerLabel = `[${logPrefix}] Build`; | ||||||
|  |     const endpoint = serverURL.pathname; | ||||||
|  |     const dispatcher = new EventTarget(); | ||||||
|  |  | ||||||
|  |     const eventServer = http.createServer((req, res) => { | ||||||
|  |         res.setHeader("Access-Control-Allow-Origin", "*"); | ||||||
|  |         res.setHeader("Access-Control-Allow-Methods", "GET"); | ||||||
|  |         res.setHeader("Access-Control-Allow-Headers", "Content-Type"); | ||||||
|  |  | ||||||
|  |         if (req.url !== endpoint) { | ||||||
|  |             console.log(`🚫 Invalid request to ${req.url}`); | ||||||
|  |             res.writeHead(404); | ||||||
|  |             res.end(); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         console.log("🔌 Client connected"); | ||||||
|  |  | ||||||
|  |         res.writeHead(200, { | ||||||
|  |             "Content-Type": "text/event-stream", | ||||||
|  |             "Cache-Control": "no-cache", | ||||||
|  |             "Connection": "keep-alive", | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |         /** | ||||||
|  |          * @param {Event} event | ||||||
|  |          */ | ||||||
|  |         const listener = (event) => { | ||||||
|  |             const body = serializeCustomEventToStream(event); | ||||||
|  |  | ||||||
|  |             res.write(body); | ||||||
|  |         }; | ||||||
|  |  | ||||||
|  |         dispatcher.addEventListener("esbuild:start", listener); | ||||||
|  |         dispatcher.addEventListener("esbuild:error", listener); | ||||||
|  |         dispatcher.addEventListener("esbuild:end", listener); | ||||||
|  |  | ||||||
|  |         req.on("close", () => { | ||||||
|  |             console.log("🔌 Client disconnected"); | ||||||
|  |  | ||||||
|  |             clearInterval(keepAliveInterval); | ||||||
|  |  | ||||||
|  |             dispatcher.removeEventListener("esbuild:start", listener); | ||||||
|  |             dispatcher.removeEventListener("esbuild:error", listener); | ||||||
|  |             dispatcher.removeEventListener("esbuild:end", listener); | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |         const keepAliveInterval = setInterval(() => { | ||||||
|  |             console.timeStamp("🏓 Keep-alive"); | ||||||
|  |  | ||||||
|  |             res.write("event: keep-alive\n\n"); | ||||||
|  |             res.write(serializeCustomEventToStream(new CustomEvent("esbuild:keep-alive"))); | ||||||
|  |         }, 15_000); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     return { | ||||||
|  |         name: "build-watcher", | ||||||
|  |         setup: (build) => { | ||||||
|  |             eventServer.listen(parseInt(serverURL.port, 10), serverURL.hostname); | ||||||
|  |  | ||||||
|  |             build.onDispose(() => { | ||||||
|  |                 eventServer.close(); | ||||||
|  |             }); | ||||||
|  |  | ||||||
|  |             build.onStart(() => { | ||||||
|  |                 console.time(timerLabel); | ||||||
|  |  | ||||||
|  |                 dispatcher.dispatchEvent( | ||||||
|  |                     new CustomEvent("esbuild:start", { | ||||||
|  |                         detail: new Date().toISOString(), | ||||||
|  |                     }), | ||||||
|  |                 ); | ||||||
|  |             }); | ||||||
|  |  | ||||||
|  |             build.onEnd((buildResult) => { | ||||||
|  |                 console.timeEnd(timerLabel); | ||||||
|  |  | ||||||
|  |                 if (!buildResult.errors.length) { | ||||||
|  |                     dispatcher.dispatchEvent( | ||||||
|  |                         new CustomEvent("esbuild:end", { | ||||||
|  |                             detail: new Date().toISOString(), | ||||||
|  |                         }), | ||||||
|  |                     ); | ||||||
|  |  | ||||||
|  |                     return; | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 console.warn(`Build ended with ${buildResult.errors.length} errors`); | ||||||
|  |  | ||||||
|  |                 dispatcher.dispatchEvent( | ||||||
|  |                     new CustomEvent("esbuild:error", { | ||||||
|  |                         detail: buildResult.errors.map((error) => ({ | ||||||
|  |                             ...error, | ||||||
|  |                             location: error.location | ||||||
|  |                                 ? { | ||||||
|  |                                       ...error.location, | ||||||
|  |                                       file: path.resolve(relativeRoot, error.location.file), | ||||||
|  |                                   } | ||||||
|  |                                 : null, | ||||||
|  |                         })), | ||||||
|  |                     }), | ||||||
|  |                 ); | ||||||
|  |             }); | ||||||
|  |         }, | ||||||
|  |     }; | ||||||
|  | } | ||||||
| @ -4,17 +4,13 @@ import { ROUTES } from "@goauthentik/admin/Routes"; | |||||||
| import { | import { | ||||||
|     EVENT_API_DRAWER_TOGGLE, |     EVENT_API_DRAWER_TOGGLE, | ||||||
|     EVENT_NOTIFICATION_DRAWER_TOGGLE, |     EVENT_NOTIFICATION_DRAWER_TOGGLE, | ||||||
|     EVENT_SIDEBAR_TOGGLE, |  | ||||||
| } from "@goauthentik/common/constants"; | } from "@goauthentik/common/constants"; | ||||||
| import { configureSentry } from "@goauthentik/common/sentry"; | import { configureSentry } from "@goauthentik/common/sentry"; | ||||||
| import { me } from "@goauthentik/common/users"; | import { me } from "@goauthentik/common/users"; | ||||||
| import { WebsocketClient } from "@goauthentik/common/ws"; | import { WebsocketClient } from "@goauthentik/common/ws"; | ||||||
| import { AuthenticatedInterface } from "@goauthentik/elements/Interface"; | import { AuthenticatedInterface } from "@goauthentik/elements/Interface"; | ||||||
| import { WithLicenseSummary } from "@goauthentik/elements/Interface/licenseSummaryProvider.js"; |  | ||||||
| import "@goauthentik/elements/ak-locale-context"; | import "@goauthentik/elements/ak-locale-context"; | ||||||
| import "@goauthentik/elements/banner/EnterpriseStatusBanner"; | import "@goauthentik/elements/banner/EnterpriseStatusBanner"; | ||||||
| import "@goauthentik/elements/banner/EnterpriseStatusBanner"; |  | ||||||
| import "@goauthentik/elements/banner/VersionBanner"; |  | ||||||
| import "@goauthentik/elements/banner/VersionBanner"; | import "@goauthentik/elements/banner/VersionBanner"; | ||||||
| import "@goauthentik/elements/messages/MessageContainer"; | import "@goauthentik/elements/messages/MessageContainer"; | ||||||
| import "@goauthentik/elements/messages/MessageContainer"; | import "@goauthentik/elements/messages/MessageContainer"; | ||||||
| @ -25,32 +21,21 @@ import "@goauthentik/elements/router/RouterOutlet"; | |||||||
| import "@goauthentik/elements/sidebar/Sidebar"; | import "@goauthentik/elements/sidebar/Sidebar"; | ||||||
| import "@goauthentik/elements/sidebar/SidebarItem"; | import "@goauthentik/elements/sidebar/SidebarItem"; | ||||||
|  |  | ||||||
| import { CSSResult, TemplateResult, css, html, nothing } from "lit"; | import { CSSResult, TemplateResult, css, html } from "lit"; | ||||||
| import { customElement, property, query, state } from "lit/decorators.js"; | import { customElement, property, query, state } from "lit/decorators.js"; | ||||||
| import { classMap } from "lit/directives/class-map.js"; | import { classMap } from "lit/directives/class-map.js"; | ||||||
|  |  | ||||||
| import PFButton from "@patternfly/patternfly/components/Button/button.css"; | import PFButton from "@patternfly/patternfly/components/Button/button.css"; | ||||||
| import PFDrawer from "@patternfly/patternfly/components/Drawer/drawer.css"; | import PFDrawer from "@patternfly/patternfly/components/Drawer/drawer.css"; | ||||||
| import PFNav from "@patternfly/patternfly/components/Nav/nav.css"; |  | ||||||
| import PFPage from "@patternfly/patternfly/components/Page/page.css"; | import PFPage from "@patternfly/patternfly/components/Page/page.css"; | ||||||
| import PFBase from "@patternfly/patternfly/patternfly-base.css"; | import PFBase from "@patternfly/patternfly/patternfly-base.css"; | ||||||
|  |  | ||||||
| import { LicenseSummaryStatusEnum, SessionUser, UiThemeEnum } from "@goauthentik/api"; | import { SessionUser, UiThemeEnum } from "@goauthentik/api"; | ||||||
|  |  | ||||||
| import { | import "./AdminSidebar"; | ||||||
|     AdminSidebarEnterpriseEntries, |  | ||||||
|     AdminSidebarEntries, |  | ||||||
|     renderSidebarItems, |  | ||||||
| } from "./AdminSidebar.js"; |  | ||||||
|  |  | ||||||
| if (process.env.NODE_ENV === "development") { |  | ||||||
|     await import("@goauthentik/esbuild-plugin-live-reload/client"); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| @customElement("ak-interface-admin") | @customElement("ak-interface-admin") | ||||||
| export class AdminInterface extends WithLicenseSummary(AuthenticatedInterface) { | export class AdminInterface extends AuthenticatedInterface { | ||||||
|     //#region Properties |  | ||||||
|  |  | ||||||
|     @property({ type: Boolean }) |     @property({ type: Boolean }) | ||||||
|     notificationDrawerOpen = getURLParam("notificationDrawerOpen", false); |     notificationDrawerOpen = getURLParam("notificationDrawerOpen", false); | ||||||
|  |  | ||||||
| @ -65,24 +50,12 @@ export class AdminInterface extends WithLicenseSummary(AuthenticatedInterface) { | |||||||
|     @query("ak-about-modal") |     @query("ak-about-modal") | ||||||
|     aboutModal?: AboutModal; |     aboutModal?: AboutModal; | ||||||
|  |  | ||||||
|     @property({ type: Boolean, reflect: true }) |  | ||||||
|     public sidebarOpen = true; |  | ||||||
|  |  | ||||||
|     #toggleSidebar = () => { |  | ||||||
|         this.sidebarOpen = !this.sidebarOpen; |  | ||||||
|     }; |  | ||||||
|  |  | ||||||
|     //#endregion |  | ||||||
|  |  | ||||||
|     //#region Styles |  | ||||||
|  |  | ||||||
|     static get styles(): CSSResult[] { |     static get styles(): CSSResult[] { | ||||||
|         return [ |         return [ | ||||||
|             PFBase, |             PFBase, | ||||||
|             PFPage, |             PFPage, | ||||||
|             PFButton, |             PFButton, | ||||||
|             PFDrawer, |             PFDrawer, | ||||||
|             PFNav, |  | ||||||
|             css` |             css` | ||||||
|                 .pf-c-page__main, |                 .pf-c-page__main, | ||||||
|                 .pf-c-drawer__content, |                 .pf-c-drawer__content, | ||||||
| @ -90,30 +63,23 @@ export class AdminInterface extends WithLicenseSummary(AuthenticatedInterface) { | |||||||
|                     z-index: auto !important; |                     z-index: auto !important; | ||||||
|                     background-color: transparent; |                     background-color: transparent; | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|                 .display-none { |                 .display-none { | ||||||
|                     display: none; |                     display: none; | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|                 .pf-c-page { |                 .pf-c-page { | ||||||
|                     background-color: var(--pf-c-page--BackgroundColor) !important; |                     background-color: var(--pf-c-page--BackgroundColor) !important; | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|                 :host([theme="dark"]) { |  | ||||||
|                 /* Global page background colour */ |                 /* Global page background colour */ | ||||||
|                     .pf-c-page { |                 :host([theme="dark"]) .pf-c-page { | ||||||
|                     --pf-c-page--BackgroundColor: var(--ak-dark-background); |                     --pf-c-page--BackgroundColor: var(--ak-dark-background); | ||||||
|                 } |                 } | ||||||
|                 } |                 ak-enterprise-status, | ||||||
|  |                 ak-version-banner { | ||||||
|                 ak-page-navbar { |  | ||||||
|                     grid-area: header; |                     grid-area: header; | ||||||
|                 } |                 } | ||||||
|  |                 ak-admin-sidebar { | ||||||
|                 .ak-sidebar { |  | ||||||
|                     grid-area: nav; |                     grid-area: nav; | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|                 .pf-c-drawer__panel { |                 .pf-c-drawer__panel { | ||||||
|                     z-index: var(--pf-global--ZIndex--xl); |                     z-index: var(--pf-global--ZIndex--xl); | ||||||
|                 } |                 } | ||||||
| @ -121,19 +87,9 @@ export class AdminInterface extends WithLicenseSummary(AuthenticatedInterface) { | |||||||
|         ]; |         ]; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     //#endregion |  | ||||||
|  |  | ||||||
|     //#region Lifecycle |  | ||||||
|  |  | ||||||
|     constructor() { |     constructor() { | ||||||
|         super(); |         super(); | ||||||
|         this.ws = new WebsocketClient(); |         this.ws = new WebsocketClient(); | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public connectedCallback(): void { |  | ||||||
|         super.connectedCallback(); |  | ||||||
|  |  | ||||||
|         window.addEventListener(EVENT_SIDEBAR_TOGGLE, this.#toggleSidebar); |  | ||||||
|  |  | ||||||
|         window.addEventListener(EVENT_NOTIFICATION_DRAWER_TOGGLE, () => { |         window.addEventListener(EVENT_NOTIFICATION_DRAWER_TOGGLE, () => { | ||||||
|             this.notificationDrawerOpen = !this.notificationDrawerOpen; |             this.notificationDrawerOpen = !this.notificationDrawerOpen; | ||||||
| @ -150,11 +106,6 @@ export class AdminInterface extends WithLicenseSummary(AuthenticatedInterface) { | |||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public disconnectedCallback(): void { |  | ||||||
|         super.disconnectedCallback(); |  | ||||||
|         window.removeEventListener(EVENT_SIDEBAR_TOGGLE, this.#toggleSidebar); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     async firstUpdated(): Promise<void> { |     async firstUpdated(): Promise<void> { | ||||||
|         configureSentry(true); |         configureSentry(true); | ||||||
|         this.user = await me(); |         this.user = await me(); | ||||||
| @ -163,22 +114,27 @@ export class AdminInterface extends WithLicenseSummary(AuthenticatedInterface) { | |||||||
|             this.user.user.isSuperuser || |             this.user.user.isSuperuser || | ||||||
|             // TODO: somehow add `access_admin_interface` to the API schema |             // TODO: somehow add `access_admin_interface` to the API schema | ||||||
|             this.user.user.systemPermissions.includes("access_admin_interface"); |             this.user.user.systemPermissions.includes("access_admin_interface"); | ||||||
|  |  | ||||||
|         if (!canAccessAdmin && this.user.user.pk > 0) { |         if (!canAccessAdmin && this.user.user.pk > 0) { | ||||||
|             window.location.assign("/if/user/"); |             window.location.assign("/if/user/"); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     async connectedCallback(): Promise<void> { | ||||||
|  |         super.connectedCallback(); | ||||||
|  |  | ||||||
|  |         if (process.env.NODE_ENV === "development" && process.env.WATCHER_URL) { | ||||||
|  |             const { ESBuildObserver } = await import("@goauthentik/common/client"); | ||||||
|  |  | ||||||
|  |             new ESBuildObserver(process.env.WATCHER_URL); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|     render(): TemplateResult { |     render(): TemplateResult { | ||||||
|         const sidebarClasses = { |         const sidebarClasses = { | ||||||
|             "pf-c-page__sidebar": true, |  | ||||||
|             "pf-m-light": this.activeTheme === UiThemeEnum.Light, |             "pf-m-light": this.activeTheme === UiThemeEnum.Light, | ||||||
|             "pf-m-expanded": this.sidebarOpen, |  | ||||||
|             "pf-m-collapsed": !this.sidebarOpen, |  | ||||||
|         }; |         }; | ||||||
|  |  | ||||||
|         const drawerOpen = this.notificationDrawerOpen || this.apiDrawerOpen; |         const drawerOpen = this.notificationDrawerOpen || this.apiDrawerOpen; | ||||||
|  |  | ||||||
|         const drawerClasses = { |         const drawerClasses = { | ||||||
|             "pf-m-expanded": drawerOpen, |             "pf-m-expanded": drawerOpen, | ||||||
|             "pf-m-collapsed": !drawerOpen, |             "pf-m-collapsed": !drawerOpen, | ||||||
| @ -186,18 +142,11 @@ export class AdminInterface extends WithLicenseSummary(AuthenticatedInterface) { | |||||||
|  |  | ||||||
|         return html` <ak-locale-context> |         return html` <ak-locale-context> | ||||||
|             <div class="pf-c-page"> |             <div class="pf-c-page"> | ||||||
|                 <ak-page-navbar> |  | ||||||
|                     <ak-version-banner></ak-version-banner> |  | ||||||
|                 <ak-enterprise-status interface="admin"></ak-enterprise-status> |                 <ak-enterprise-status interface="admin"></ak-enterprise-status> | ||||||
|                 </ak-page-navbar> |                 <ak-version-banner></ak-version-banner> | ||||||
|  |                 <ak-admin-sidebar | ||||||
|                 <ak-sidebar class="${classMap(sidebarClasses)}"> |                     class="pf-c-page__sidebar ${classMap(sidebarClasses)}" | ||||||
|                     ${renderSidebarItems(AdminSidebarEntries)} |                 ></ak-admin-sidebar> | ||||||
|                     ${this.licenseSummary?.status !== LicenseSummaryStatusEnum.Unlicensed |  | ||||||
|                         ? renderSidebarItems(AdminSidebarEnterpriseEntries) |  | ||||||
|                         : nothing} |  | ||||||
|                 </ak-sidebar> |  | ||||||
|  |  | ||||||
|                 <div class="pf-c-page__drawer"> |                 <div class="pf-c-page__drawer"> | ||||||
|                     <div class="pf-c-drawer ${classMap(drawerClasses)}"> |                     <div class="pf-c-drawer ${classMap(drawerClasses)}"> | ||||||
|                         <div class="pf-c-drawer__main"> |                         <div class="pf-c-drawer__main"> | ||||||
|  | |||||||
| @ -1,10 +1,100 @@ | |||||||
|  | import { EVENT_SIDEBAR_TOGGLE } from "@goauthentik/common/constants"; | ||||||
|  | import { me } from "@goauthentik/common/users"; | ||||||
|  | import { AKElement } from "@goauthentik/elements/Base"; | ||||||
|  | import { | ||||||
|  |     CapabilitiesEnum, | ||||||
|  |     WithCapabilitiesConfig, | ||||||
|  | } from "@goauthentik/elements/Interface/capabilitiesProvider"; | ||||||
|  | import { WithVersion } from "@goauthentik/elements/Interface/versionProvider"; | ||||||
| import { ID_REGEX, SLUG_REGEX, UUID_REGEX } from "@goauthentik/elements/router/Route"; | import { ID_REGEX, SLUG_REGEX, UUID_REGEX } from "@goauthentik/elements/router/Route"; | ||||||
|  | import { getRootStyle } from "@goauthentik/elements/utils/getRootStyle"; | ||||||
| import { spread } from "@open-wc/lit-helpers"; | import { spread } from "@open-wc/lit-helpers"; | ||||||
|  |  | ||||||
| import { msg } from "@lit/localize"; | import { msg } from "@lit/localize"; | ||||||
| import { TemplateResult, html, nothing } from "lit"; | import { TemplateResult, html, nothing } from "lit"; | ||||||
| import { repeat } from "lit/directives/repeat.js"; | import { customElement, property, state } from "lit/decorators.js"; | ||||||
|  | import { map } from "lit/directives/map.js"; | ||||||
|  |  | ||||||
|  | import { UiThemeEnum } from "@goauthentik/api"; | ||||||
|  | import type { SessionUser, UserSelf } from "@goauthentik/api"; | ||||||
|  |  | ||||||
|  | @customElement("ak-admin-sidebar") | ||||||
|  | export class AkAdminSidebar extends WithCapabilitiesConfig(WithVersion(AKElement)) { | ||||||
|  |     @property({ type: Boolean, reflect: true }) | ||||||
|  |     open = true; | ||||||
|  |  | ||||||
|  |     @state() | ||||||
|  |     impersonation: UserSelf["username"] | null = null; | ||||||
|  |  | ||||||
|  |     constructor() { | ||||||
|  |         super(); | ||||||
|  |         me().then((user: SessionUser) => { | ||||||
|  |             this.impersonation = user.original ? user.user.username : null; | ||||||
|  |         }); | ||||||
|  |         this.toggleOpen = this.toggleOpen.bind(this); | ||||||
|  |         this.checkWidth = this.checkWidth.bind(this); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // This has to be a bound method so the event listener can be removed on disconnection as | ||||||
|  |     // needed. | ||||||
|  |     toggleOpen() { | ||||||
|  |         this.open = !this.open; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     checkWidth() { | ||||||
|  |         // This works just fine, but it assumes that the `--ak-sidebar--minimum-auto-width` is in | ||||||
|  |         // REMs. If that changes, this code will have to be adjusted as well. | ||||||
|  |         const minWidth = | ||||||
|  |             parseFloat(getRootStyle("--ak-sidebar--minimum-auto-width")) * | ||||||
|  |             parseFloat(getRootStyle("font-size")); | ||||||
|  |         this.open = window.innerWidth >= minWidth; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     connectedCallback() { | ||||||
|  |         super.connectedCallback(); | ||||||
|  |         window.addEventListener(EVENT_SIDEBAR_TOGGLE, this.toggleOpen); | ||||||
|  |         window.addEventListener("resize", this.checkWidth); | ||||||
|  |         // After connecting to the DOM, we can now perform this check to see if the sidebar should | ||||||
|  |         // be open by default. | ||||||
|  |         this.checkWidth(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // The symmetry (☟, ☝) here is critical in that you want to start adding these handlers after | ||||||
|  |     // connection, and removing them before disconnection. | ||||||
|  |  | ||||||
|  |     disconnectedCallback() { | ||||||
|  |         window.removeEventListener(EVENT_SIDEBAR_TOGGLE, this.toggleOpen); | ||||||
|  |         window.removeEventListener("resize", this.checkWidth); | ||||||
|  |         super.disconnectedCallback(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     render() { | ||||||
|  |         return html` | ||||||
|  |             <ak-sidebar | ||||||
|  |                 class="pf-c-page__sidebar ${this.open ? "pf-m-expanded" : "pf-m-collapsed"} ${this | ||||||
|  |                     .activeTheme === UiThemeEnum.Light | ||||||
|  |                     ? "pf-m-light" | ||||||
|  |                     : ""}" | ||||||
|  |             > | ||||||
|  |                 ${this.renderSidebarItems()} | ||||||
|  |             </ak-sidebar> | ||||||
|  |         `; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     updated() { | ||||||
|  |         // This is permissible as`:host.classList` is not one of the properties Lit uses as a | ||||||
|  |         // scheduling trigger. This sort of shenanigans can trigger an loop, in that it will trigger | ||||||
|  |         // a browser reflow, which may trigger some other styling the application is monitoring, | ||||||
|  |         // triggering a re-render which triggers a browser reflow, ad infinitum. But we've been | ||||||
|  |         // living with that since jQuery, and it's both well-known and fortunately rare. | ||||||
|  |  | ||||||
|  |         // eslint-disable-next-line wc/no-self-class | ||||||
|  |         this.classList.remove("pf-m-expanded", "pf-m-collapsed"); | ||||||
|  |         // eslint-disable-next-line wc/no-self-class | ||||||
|  |         this.classList.add(this.open ? "pf-m-expanded" : "pf-m-collapsed"); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     renderSidebarItems(): TemplateResult { | ||||||
|         // The second attribute type is of string[] to help with the 'activeWhen' control, which was |         // The second attribute type is of string[] to help with the 'activeWhen' control, which was | ||||||
|         // commonplace and singular enough to merit its own handler. |         // commonplace and singular enough to merit its own handler. | ||||||
|         type SidebarEntry = [ |         type SidebarEntry = [ | ||||||
| @ -14,65 +104,29 @@ type SidebarEntry = [ | |||||||
|             children?: SidebarEntry[], |             children?: SidebarEntry[], | ||||||
|         ]; |         ]; | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Recursively renders a sidebar entry. |  | ||||||
|  */ |  | ||||||
| export function renderSidebarItem([ |  | ||||||
|     path, |  | ||||||
|     label, |  | ||||||
|     attributes, |  | ||||||
|     children, |  | ||||||
| ]: SidebarEntry): TemplateResult { |  | ||||||
|     const properties = Array.isArray(attributes) |  | ||||||
|         ? { ".activeWhen": attributes } |  | ||||||
|         : (attributes ?? {}); |  | ||||||
|  |  | ||||||
|     if (path) { |  | ||||||
|         properties.path = path; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     return html`<ak-sidebar-item ${spread(properties)}> |  | ||||||
|         ${label ? html`<span slot="label">${label}</span>` : nothing} |  | ||||||
|         ${children ? renderSidebarItems(children) : nothing} |  | ||||||
|     </ak-sidebar-item>`; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Recursively renders a collection of sidebar entries. |  | ||||||
|  */ |  | ||||||
| export function renderSidebarItems(entries: readonly SidebarEntry[]) { |  | ||||||
|     console.debug("authentik/sidebar: Rendering sidebar items", entries); |  | ||||||
|     return repeat(entries, ([path, label]) => path || label, renderSidebarItem); |  | ||||||
| } |  | ||||||
|  |  | ||||||
|         // prettier-ignore |         // prettier-ignore | ||||||
| export const AdminSidebarEntries: readonly SidebarEntry[] = [ |         const sidebarContent: SidebarEntry[] = [ | ||||||
|             [null, msg("Dashboards"), { "?expanded": true }, [ |             [null, msg("Dashboards"), { "?expanded": true }, [ | ||||||
|                 ["/administration/overview", msg("Overview")], |                 ["/administration/overview", msg("Overview")], | ||||||
|                 ["/administration/dashboard/users", msg("User Statistics")], |                 ["/administration/dashboard/users", msg("User Statistics")], | ||||||
|         ["/administration/system-tasks", msg("System Tasks")]] |                 ["/administration/system-tasks", msg("System Tasks")]]], | ||||||
|     ], |  | ||||||
|             [null, msg("Applications"), null, [ |             [null, msg("Applications"), null, [ | ||||||
|                 ["/core/applications", msg("Applications"), [`^/core/applications/(?<slug>${SLUG_REGEX})$`]], |                 ["/core/applications", msg("Applications"), [`^/core/applications/(?<slug>${SLUG_REGEX})$`]], | ||||||
|                 ["/core/providers", msg("Providers"), [`^/core/providers/(?<id>${ID_REGEX})$`]], |                 ["/core/providers", msg("Providers"), [`^/core/providers/(?<id>${ID_REGEX})$`]], | ||||||
|         ["/outpost/outposts", msg("Outposts")]] |                 ["/outpost/outposts", msg("Outposts")]]], | ||||||
|     ], |  | ||||||
|             [null, msg("Events"), null, [ |             [null, msg("Events"), null, [ | ||||||
|                 ["/events/log", msg("Logs"), [`^/events/log/(?<id>${UUID_REGEX})$`]], |                 ["/events/log", msg("Logs"), [`^/events/log/(?<id>${UUID_REGEX})$`]], | ||||||
|                 ["/events/rules", msg("Notification Rules")], |                 ["/events/rules", msg("Notification Rules")], | ||||||
|         ["/events/transports", msg("Notification Transports")]] |                 ["/events/transports", msg("Notification Transports")]]], | ||||||
|     ], |  | ||||||
|             [null, msg("Customization"), null, [ |             [null, msg("Customization"), null, [ | ||||||
|                 ["/policy/policies", msg("Policies")], |                 ["/policy/policies", msg("Policies")], | ||||||
|                 ["/core/property-mappings", msg("Property Mappings")], |                 ["/core/property-mappings", msg("Property Mappings")], | ||||||
|                 ["/blueprints/instances", msg("Blueprints")], |                 ["/blueprints/instances", msg("Blueprints")], | ||||||
|         ["/policy/reputation", msg("Reputation scores")]] |                 ["/policy/reputation", msg("Reputation scores")]]], | ||||||
|     ], |  | ||||||
|             [null, msg("Flows and Stages"), null, [ |             [null, msg("Flows and Stages"), null, [ | ||||||
|                 ["/flow/flows", msg("Flows"), [`^/flow/flows/(?<slug>${SLUG_REGEX})$`]], |                 ["/flow/flows", msg("Flows"), [`^/flow/flows/(?<slug>${SLUG_REGEX})$`]], | ||||||
|                 ["/flow/stages", msg("Stages")], |                 ["/flow/stages", msg("Stages")], | ||||||
|         ["/flow/stages/prompts", msg("Prompts")]] |                 ["/flow/stages/prompts", msg("Prompts")]]], | ||||||
|     ], |  | ||||||
|             [null, msg("Directory"), null, [ |             [null, msg("Directory"), null, [ | ||||||
|                 ["/identity/users", msg("Users"), [`^/identity/users/(?<id>${ID_REGEX})$`]], |                 ["/identity/users", msg("Users"), [`^/identity/users/(?<id>${ID_REGEX})$`]], | ||||||
|                 ["/identity/groups", msg("Groups"), [`^/identity/groups/(?<id>${UUID_REGEX})$`]], |                 ["/identity/groups", msg("Groups"), [`^/identity/groups/(?<id>${UUID_REGEX})$`]], | ||||||
| @ -80,19 +134,53 @@ export const AdminSidebarEntries: readonly SidebarEntry[] = [ | |||||||
|                 ["/identity/initial-permissions", msg("Initial Permissions"), [`^/identity/initial-permissions/(?<id>${ID_REGEX})$`]], |                 ["/identity/initial-permissions", msg("Initial Permissions"), [`^/identity/initial-permissions/(?<id>${ID_REGEX})$`]], | ||||||
|                 ["/core/sources", msg("Federation and Social login"), [`^/core/sources/(?<slug>${SLUG_REGEX})$`]], |                 ["/core/sources", msg("Federation and Social login"), [`^/core/sources/(?<slug>${SLUG_REGEX})$`]], | ||||||
|                 ["/core/tokens", msg("Tokens and App passwords")], |                 ["/core/tokens", msg("Tokens and App passwords")], | ||||||
|         ["/flow/stages/invitations", msg("Invitations")]] |                 ["/flow/stages/invitations", msg("Invitations")]]], | ||||||
|     ], |  | ||||||
|             [null, msg("System"), null, [ |             [null, msg("System"), null, [ | ||||||
|                 ["/core/brands", msg("Brands")], |                 ["/core/brands", msg("Brands")], | ||||||
|                 ["/crypto/certificates", msg("Certificates")], |                 ["/crypto/certificates", msg("Certificates")], | ||||||
|                 ["/outpost/integrations", msg("Outpost Integrations")], |                 ["/outpost/integrations", msg("Outpost Integrations")], | ||||||
|         ["/admin/settings", msg("Settings")]] |                 ["/admin/settings", msg("Settings")]]], | ||||||
|     ], |  | ||||||
|         ]; |         ]; | ||||||
|  |  | ||||||
|  |         // Typescript requires the type here to correctly type the recursive path | ||||||
|  |         type SidebarRenderer = (_: SidebarEntry) => TemplateResult; | ||||||
|  |  | ||||||
|  |         const renderOneSidebarItem: SidebarRenderer = ([path, label, attributes, children]) => { | ||||||
|  |             const properties = Array.isArray(attributes) | ||||||
|  |                 ? { ".activeWhen": attributes } | ||||||
|  |                 : (attributes ?? {}); | ||||||
|  |             if (path) { | ||||||
|  |                 properties.path = path; | ||||||
|  |             } | ||||||
|  |             return html`<ak-sidebar-item ${spread(properties)}> | ||||||
|  |                 ${label ? html`<span slot="label">${label}</span>` : nothing} | ||||||
|  |                 ${map(children, renderOneSidebarItem)} | ||||||
|  |             </ak-sidebar-item>`; | ||||||
|  |         }; | ||||||
|  |  | ||||||
|         // prettier-ignore |         // prettier-ignore | ||||||
| export const AdminSidebarEnterpriseEntries: readonly SidebarEntry[] = [ |         return html` | ||||||
|     [null, msg("Enterprise"), null, [ |             ${map(sidebarContent, renderOneSidebarItem)} | ||||||
|         ["/enterprise/licenses", msg("Licenses"), null] |             ${this.renderEnterpriseMenu()} | ||||||
|     ], |         `; | ||||||
| ]] |     } | ||||||
|  |  | ||||||
|  |     renderEnterpriseMenu() { | ||||||
|  |         return this.can(CapabilitiesEnum.IsEnterprise) | ||||||
|  |             ? html` | ||||||
|  |                   <ak-sidebar-item> | ||||||
|  |                       <span slot="label">${msg("Enterprise")}</span> | ||||||
|  |                       <ak-sidebar-item path="/enterprise/licenses"> | ||||||
|  |                           <span slot="label">${msg("Licenses")}</span> | ||||||
|  |                       </ak-sidebar-item> | ||||||
|  |                   </ak-sidebar-item> | ||||||
|  |               ` | ||||||
|  |             : nothing; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | declare global { | ||||||
|  |     interface HTMLElementTagNameMap { | ||||||
|  |         "ak-admin-sidebar": AkAdminSidebar; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | |||||||
| @ -58,6 +58,9 @@ export class AdminOverviewPage extends AdminOverviewBase { | |||||||
|             PFContent, |             PFContent, | ||||||
|             PFDivider, |             PFDivider, | ||||||
|             css` |             css` | ||||||
|  |                 .pf-l-grid__item { | ||||||
|  |                     height: 100%; | ||||||
|  |                 } | ||||||
|                 .pf-l-grid__item.big-graph-container { |                 .pf-l-grid__item.big-graph-container { | ||||||
|                     height: 35em; |                     height: 35em; | ||||||
|                 } |                 } | ||||||
| @ -71,10 +74,6 @@ export class AdminOverviewPage extends AdminOverviewBase { | |||||||
|                     line-height: normal; |                     line-height: normal; | ||||||
|                     font-size: var(--pf-global--icon--FontSize--sm); |                     font-size: var(--pf-global--icon--FontSize--sm); | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|                 .chart-item { |  | ||||||
|                     aspect-ratio: 2 / 1; |  | ||||||
|                 } |  | ||||||
|             `, |             `, | ||||||
|         ]; |         ]; | ||||||
|     } |     } | ||||||
| @ -95,31 +94,22 @@ export class AdminOverviewPage extends AdminOverviewBase { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     render(): TemplateResult { |     render(): TemplateResult { | ||||||
|         const username = this.user?.user.name || this.user?.user.username; |         const name = this.user?.user.name ?? this.user?.user.username; | ||||||
|  |  | ||||||
|         return html` <ak-page-header |         return html`<ak-page-header description=${msg("General system status")} ?hasIcon=${false}> | ||||||
|                 header=${msg(str`Welcome, ${username || ""}.`)} |                 <span slot="header"> ${msg(str`Welcome, ${name || ""}.`)} </span> | ||||||
|                 description=${msg("General system status")} |  | ||||||
|                 ?hasIcon=${false} |  | ||||||
|             > |  | ||||||
|             </ak-page-header> |             </ak-page-header> | ||||||
|             <section class="pf-c-page__main-section"> |             <section class="pf-c-page__main-section"> | ||||||
|                 <div class="pf-l-grid pf-m-gutter"> |                 <div class="pf-l-grid pf-m-gutter"> | ||||||
|                     <div class="pf-l-grid__item pf-m-12-col pf-m-2-row pf-m-9-col-on-xl"> |                     <!-- row 1 --> | ||||||
|                         <ak-recent-events pageSize="6"></ak-recent-events> |                     <div | ||||||
|                     </div> |                         class="pf-l-grid__item pf-m-12-col pf-m-6-col-on-xl pf-m-6-col-on-2xl pf-l-grid pf-m-gutter" | ||||||
|                     <div class="pf-l-grid__item pf-m-12-col pf-m-6-col-on-sm pf-m-3-col-on-xl"> |                     > | ||||||
|  |                         <div class="pf-l-grid__item pf-m-12-col pf-m-6-col-on-xl pf-m-4-col-on-2xl"> | ||||||
|                             <ak-quick-actions-card .actions=${this.quickActions}> |                             <ak-quick-actions-card .actions=${this.quickActions}> | ||||||
|                             </ak-quick-actions-card> |                             </ak-quick-actions-card> | ||||||
|                         </div> |                         </div> | ||||||
|  |                         <div class="pf-l-grid__item pf-m-12-col pf-m-6-col-on-xl pf-m-4-col-on-2xl"> | ||||||
|                     <div class="pf-l-grid__item pf-m-12-col pf-m-6-col-on-sm pf-m-3-col-on-xl"> |  | ||||||
|                         <ak-admin-status-version> </ak-admin-status-version> |  | ||||||
|                     </div> |  | ||||||
|                     <div class="pf-l-grid pf-l-grid__item pf-m-12-col pf-m-gutter"> |  | ||||||
|                         ${this.renderSecondaryRow()} |  | ||||||
|  |  | ||||||
|                         <div class="pf-l-grid__item pf-m-12-col pf-m-6-col-on-md chart-item"> |  | ||||||
|                             <ak-aggregate-card |                             <ak-aggregate-card | ||||||
|                                 icon="pf-icon pf-icon-zone" |                                 icon="pf-icon pf-icon-zone" | ||||||
|                                 header=${msg("Outpost status")} |                                 header=${msg("Outpost status")} | ||||||
| @ -128,13 +118,24 @@ export class AdminOverviewPage extends AdminOverviewBase { | |||||||
|                                 <ak-admin-status-chart-outpost></ak-admin-status-chart-outpost> |                                 <ak-admin-status-chart-outpost></ak-admin-status-chart-outpost> | ||||||
|                             </ak-aggregate-card> |                             </ak-aggregate-card> | ||||||
|                         </div> |                         </div> | ||||||
|                         <div class="pf-l-grid__item pf-m-12-col pf-m-6-col-on-md chart-item"> |                         <div | ||||||
|  |                             class="pf-l-grid__item pf-m-12-col pf-m-12-col-on-xl pf-m-4-col-on-2xl" | ||||||
|  |                         > | ||||||
|                             <ak-aggregate-card icon="fa fa-sync-alt" header=${msg("Sync status")}> |                             <ak-aggregate-card icon="fa fa-sync-alt" header=${msg("Sync status")}> | ||||||
|                                 <ak-admin-status-chart-sync></ak-admin-status-chart-sync> |                                 <ak-admin-status-chart-sync></ak-admin-status-chart-sync> | ||||||
|                             </ak-aggregate-card> |                             </ak-aggregate-card> | ||||||
|                         </div> |                         </div> | ||||||
|  |                         <div class="pf-l-grid__item pf-m-12-col"> | ||||||
|  |                             <hr class="pf-c-divider" /> | ||||||
|  |                         </div> | ||||||
|  |                         ${this.renderCards()} | ||||||
|  |                     </div> | ||||||
|  |                     <div class="pf-l-grid__item pf-m-12-col pf-m-6-col-on-xl"> | ||||||
|  |                         <ak-recent-events pageSize="6"></ak-recent-events> | ||||||
|  |                     </div> | ||||||
|  |                     <div class="pf-l-grid__item pf-m-12-col"> | ||||||
|  |                         <hr class="pf-c-divider" /> | ||||||
|                     </div> |                     </div> | ||||||
|  |  | ||||||
|                     <!-- row 3 --> |                     <!-- row 3 --> | ||||||
|                     <div |                     <div | ||||||
|                         class="pf-l-grid__item pf-m-12-col pf-m-6-col-on-xl pf-m-8-col-on-2xl big-graph-container" |                         class="pf-l-grid__item pf-m-12-col pf-m-6-col-on-xl pf-m-8-col-on-2xl big-graph-container" | ||||||
| @ -162,34 +163,32 @@ export class AdminOverviewPage extends AdminOverviewBase { | |||||||
|             </section>`; |             </section>`; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     renderSecondaryRow() { |     renderCards() { | ||||||
|         const isEnterprise = this.hasEnterpriseLicense; |         const isEnterprise = this.hasEnterpriseLicense; | ||||||
|         const colSpan = isEnterprise ? 4 : 6; |  | ||||||
|  |  | ||||||
|         const classes = { |         const classes = { | ||||||
|             "card-container": true, |             "card-container": true, | ||||||
|             "pf-l-grid__item": true, |             "pf-l-grid__item": true, | ||||||
|             [`pf-m-12-col`]: true, |             "pf-m-6-col": true, | ||||||
|             [`pf-m-${colSpan}-col-on-md`]: true, |             "pf-m-4-col-on-md": !isEnterprise, | ||||||
|  |             "pf-m-4-col-on-xl": !isEnterprise, | ||||||
|  |             "pf-m-3-col-on-md": isEnterprise, | ||||||
|  |             "pf-m-3-col-on-xl": isEnterprise, | ||||||
|         }; |         }; | ||||||
|  |  | ||||||
|         return html` |         return html`<div class=${classMap(classes)}> | ||||||
|             <div class=${classMap(classes)}> |  | ||||||
|                 <ak-admin-status-system> </ak-admin-status-system> |                 <ak-admin-status-system> </ak-admin-status-system> | ||||||
|             </div> |             </div> | ||||||
|  |             <div class=${classMap(classes)}> | ||||||
|  |                 <ak-admin-status-version> </ak-admin-status-version> | ||||||
|  |             </div> | ||||||
|             <div class=${classMap(classes)}> |             <div class=${classMap(classes)}> | ||||||
|                 <ak-admin-status-card-workers> </ak-admin-status-card-workers> |                 <ak-admin-status-card-workers> </ak-admin-status-card-workers> | ||||||
|             </div> |             </div> | ||||||
|  |  | ||||||
|             ${isEnterprise |             ${isEnterprise | ||||||
|                 ? html` |                 ? html` <div class=${classMap(classes)}> | ||||||
|                       <div class=${classMap(classes)}> |  | ||||||
|                       <ak-admin-fips-status-system> </ak-admin-fips-status-system> |                       <ak-admin-fips-status-system> </ak-admin-fips-status-system> | ||||||
|                       </div> |                   </div>` | ||||||
|                   ` |                 : nothing} `; | ||||||
|                 : nothing} |  | ||||||
|         `; |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     renderActions() { |     renderActions() { | ||||||
|  | |||||||
| @ -83,10 +83,13 @@ export class AdminSettingsPage extends AKElement { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     render() { |     render() { | ||||||
|         if (!this.settings) return nothing; |         if (!this.settings) { | ||||||
|  |             return nothing; | ||||||
|  |         } | ||||||
|         return html` |         return html` | ||||||
|             <ak-page-header icon="fa fa-cog" header="${msg("System settings")}"> </ak-page-header> |             <ak-page-header icon="fa fa-cog" header="" description=""> | ||||||
|  |                 <span slot="header"> ${msg("System settings")} </span> | ||||||
|  |             </ak-page-header> | ||||||
|             <section class="pf-c-page__main-section pf-m-no-padding-mobile pf-l-grid pf-m-gutter"> |             <section class="pf-c-page__main-section pf-m-no-padding-mobile pf-l-grid pf-m-gutter"> | ||||||
|                 <div class="pf-c-card"> |                 <div class="pf-c-card"> | ||||||
|                     <div class="pf-c-card__body"> |                     <div class="pf-c-card__body"> | ||||||
|  | |||||||
| @ -52,6 +52,7 @@ function renderRadiusOverview(rawProvider: OneOfProvider) { | |||||||
| } | } | ||||||
|  |  | ||||||
| function renderRACOverview(rawProvider: OneOfProvider) { | function renderRACOverview(rawProvider: OneOfProvider) { | ||||||
|  |     // @ts-expect-error TS6133 | ||||||
|     const _provider = rawProvider as RACProvider; |     const _provider = rawProvider as RACProvider; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | |||||||
| @ -61,6 +61,7 @@ export class BoundPoliciesList extends Table<PolicyBinding> { | |||||||
|             new TableColumn(this.allowedTypesLabel), |             new TableColumn(this.allowedTypesLabel), | ||||||
|             new TableColumn(msg("Enabled"), "enabled"), |             new TableColumn(msg("Enabled"), "enabled"), | ||||||
|             new TableColumn(msg("Timeout"), "timeout"), |             new TableColumn(msg("Timeout"), "timeout"), | ||||||
|  |             new TableColumn(msg("Honor order"), "honor_order"), | ||||||
|             new TableColumn(msg("Actions")), |             new TableColumn(msg("Actions")), | ||||||
|         ]; |         ]; | ||||||
|     } |     } | ||||||
| @ -165,6 +166,7 @@ export class BoundPoliciesList extends Table<PolicyBinding> { | |||||||
|             html`${this.getPolicyUserGroupRow(item)}`, |             html`${this.getPolicyUserGroupRow(item)}`, | ||||||
|             html`<ak-status-label type="warning" ?good=${item.enabled}></ak-status-label>`, |             html`<ak-status-label type="warning" ?good=${item.enabled}></ak-status-label>`, | ||||||
|             html`${item.timeout}`, |             html`${item.timeout}`, | ||||||
|  |             html`<ak-status-label type="info" ?good=${item.honorOrder}></ak-status-label>`, | ||||||
|             html` ${this.getObjectEditButton(item)} |             html` ${this.getObjectEditButton(item)} | ||||||
|                 <ak-forms-modal size=${PFSize.Medium}> |                 <ak-forms-modal size=${PFSize.Medium}> | ||||||
|                     <span slot="submit"> ${msg("Update")} </span> |                     <span slot="submit"> ${msg("Update")} </span> | ||||||
|  | |||||||
| @ -310,6 +310,26 @@ export class PolicyBindingForm extends ModelForm<PolicyBinding, string> { | |||||||
|                     required |                     required | ||||||
|                 /> |                 /> | ||||||
|             </ak-form-element-horizontal> |             </ak-form-element-horizontal> | ||||||
|  |             <ak-form-element-horizontal name="honorOrder"> | ||||||
|  |                 <label class="pf-c-switch"> | ||||||
|  |                     <input | ||||||
|  |                         class="pf-c-switch__input" | ||||||
|  |                         type="checkbox" | ||||||
|  |                         ?checked=${first(this.instance?.honorOrder, false)} | ||||||
|  |                     /> | ||||||
|  |                     <span class="pf-c-switch__toggle"> | ||||||
|  |                         <span class="pf-c-switch__toggle-icon"> | ||||||
|  |                             <i class="fas fa-check" aria-hidden="true"></i> | ||||||
|  |                         </span> | ||||||
|  |                     </span> | ||||||
|  |                     <span class="pf-c-switch__label">${msg("Honor order")}</span> | ||||||
|  |                 </label> | ||||||
|  |                 <p class="pf-c-form__helper-text"> | ||||||
|  |                     ${msg( | ||||||
|  |                         "Honor the order of policies. Use if policies must be evaluated sequentially following the specified order. May impact performance.", | ||||||
|  |                     )} | ||||||
|  |                 </p> | ||||||
|  |             </ak-form-element-horizontal> | ||||||
|             <ak-form-element-horizontal label=${msg("Timeout")} ?required=${true} name="timeout"> |             <ak-form-element-horizontal label=${msg("Timeout")} ?required=${true} name="timeout"> | ||||||
|                 <input |                 <input | ||||||
|                     type="number" |                     type="number" | ||||||
|  | |||||||
| @ -6,7 +6,6 @@ import "@goauthentik/admin/policies/expiry/ExpiryPolicyForm"; | |||||||
| import "@goauthentik/admin/policies/expression/ExpressionPolicyForm"; | import "@goauthentik/admin/policies/expression/ExpressionPolicyForm"; | ||||||
| import "@goauthentik/admin/policies/password/PasswordPolicyForm"; | import "@goauthentik/admin/policies/password/PasswordPolicyForm"; | ||||||
| import "@goauthentik/admin/policies/reputation/ReputationPolicyForm"; | import "@goauthentik/admin/policies/reputation/ReputationPolicyForm"; | ||||||
| import "@goauthentik/admin/policies/unique_password/UniquePasswordPolicyForm"; |  | ||||||
| import "@goauthentik/admin/rbac/ObjectPermissionModal"; | import "@goauthentik/admin/rbac/ObjectPermissionModal"; | ||||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||||
| import { PFColor } from "@goauthentik/elements/Label"; | import { PFColor } from "@goauthentik/elements/Label"; | ||||||
|  | |||||||
| @ -6,7 +6,6 @@ import "@goauthentik/admin/policies/expression/ExpressionPolicyForm"; | |||||||
| import "@goauthentik/admin/policies/geoip/GeoIPPolicyForm"; | import "@goauthentik/admin/policies/geoip/GeoIPPolicyForm"; | ||||||
| import "@goauthentik/admin/policies/password/PasswordPolicyForm"; | import "@goauthentik/admin/policies/password/PasswordPolicyForm"; | ||||||
| import "@goauthentik/admin/policies/reputation/ReputationPolicyForm"; | import "@goauthentik/admin/policies/reputation/ReputationPolicyForm"; | ||||||
| import "@goauthentik/admin/policies/unique_password/UniquePasswordPolicyForm"; |  | ||||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||||
| import { AKElement } from "@goauthentik/elements/Base"; | import { AKElement } from "@goauthentik/elements/Base"; | ||||||
| import "@goauthentik/elements/forms/ProxyForm"; | import "@goauthentik/elements/forms/ProxyForm"; | ||||||
|  | |||||||
| @ -1,103 +0,0 @@ | |||||||
| import { BasePolicyForm } from "@goauthentik/admin/policies/BasePolicyForm"; |  | ||||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; |  | ||||||
| import { first } from "@goauthentik/common/utils"; |  | ||||||
| import "@goauthentik/elements/forms/FormGroup"; |  | ||||||
| import "@goauthentik/elements/forms/HorizontalFormElement"; |  | ||||||
|  |  | ||||||
| import { msg } from "@lit/localize"; |  | ||||||
| import { TemplateResult, html } from "lit"; |  | ||||||
| import { customElement } from "lit/decorators.js"; |  | ||||||
| import { ifDefined } from "lit/directives/if-defined.js"; |  | ||||||
|  |  | ||||||
| import { PoliciesApi, UniquePasswordPolicy } from "@goauthentik/api"; |  | ||||||
|  |  | ||||||
| @customElement("ak-policy-password-uniqueness-form") |  | ||||||
| export class UniquePasswordPolicyForm extends BasePolicyForm<UniquePasswordPolicy> { |  | ||||||
|     async loadInstance(pk: string): Promise<UniquePasswordPolicy> { |  | ||||||
|         return new PoliciesApi(DEFAULT_CONFIG).policiesUniquePasswordRetrieve({ |  | ||||||
|             policyUuid: pk, |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     async send(data: UniquePasswordPolicy): Promise<UniquePasswordPolicy> { |  | ||||||
|         if (this.instance) { |  | ||||||
|             return new PoliciesApi(DEFAULT_CONFIG).policiesUniquePasswordUpdate({ |  | ||||||
|                 policyUuid: this.instance.pk || "", |  | ||||||
|                 uniquePasswordPolicyRequest: data, |  | ||||||
|             }); |  | ||||||
|         } else { |  | ||||||
|             return new PoliciesApi(DEFAULT_CONFIG).policiesUniquePasswordCreate({ |  | ||||||
|                 uniquePasswordPolicyRequest: data, |  | ||||||
|             }); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     renderForm(): TemplateResult { |  | ||||||
|         return html` <span> |  | ||||||
|                 ${msg( |  | ||||||
|                     "Ensure that the user's new password is different from their previous passwords. The number of past passwords to check is configurable.", |  | ||||||
|                 )} |  | ||||||
|             </span> |  | ||||||
|             <ak-form-element-horizontal label=${msg("Name")} ?required=${true} name="name"> |  | ||||||
|                 <input |  | ||||||
|                     type="text" |  | ||||||
|                     value="${ifDefined(this.instance?.name || "")}" |  | ||||||
|                     class="pf-c-form-control" |  | ||||||
|                     required |  | ||||||
|                 /> |  | ||||||
|             </ak-form-element-horizontal> |  | ||||||
|             <ak-form-element-horizontal name="executionLogging"> |  | ||||||
|                 <label class="pf-c-switch"> |  | ||||||
|                     <input |  | ||||||
|                         class="pf-c-switch__input" |  | ||||||
|                         type="checkbox" |  | ||||||
|                         ?checked=${first(this.instance?.executionLogging, false)} |  | ||||||
|                     /> |  | ||||||
|                     <span class="pf-c-switch__toggle"> |  | ||||||
|                         <span class="pf-c-switch__toggle-icon"> |  | ||||||
|                             <i class="fas fa-check" aria-hidden="true"></i> |  | ||||||
|                         </span> |  | ||||||
|                     </span> |  | ||||||
|                     <span class="pf-c-switch__label">${msg("Execution logging")}</span> |  | ||||||
|                 </label> |  | ||||||
|                 <p class="pf-c-form__helper-text"> |  | ||||||
|                     ${msg( |  | ||||||
|                         "When this option is enabled, all executions of this policy will be logged. By default, only execution errors are logged.", |  | ||||||
|                     )} |  | ||||||
|                 </p> |  | ||||||
|             </ak-form-element-horizontal> |  | ||||||
|             <ak-form-element-horizontal |  | ||||||
|                 label=${msg("Password field")} |  | ||||||
|                 ?required=${true} |  | ||||||
|                 name="passwordField" |  | ||||||
|             > |  | ||||||
|                 <input |  | ||||||
|                     type="text" |  | ||||||
|                     value="${ifDefined(this.instance?.passwordField || "password")}" |  | ||||||
|                     class="pf-c-form-control" |  | ||||||
|                     required |  | ||||||
|                 /> |  | ||||||
|                 <p class="pf-c-form__helper-text"> |  | ||||||
|                     ${msg("Field key to check, field keys defined in Prompt stages are available.")} |  | ||||||
|                 </p> |  | ||||||
|             </ak-form-element-horizontal> |  | ||||||
|             <ak-form-element-horizontal |  | ||||||
|                 label=${msg("Number of previous passwords to check")} |  | ||||||
|                 ?required=${true} |  | ||||||
|                 name="numHistoricalPasswords" |  | ||||||
|             > |  | ||||||
|                 <input |  | ||||||
|                     type="number" |  | ||||||
|                     value="${first(this.instance?.numHistoricalPasswords, 1)}" |  | ||||||
|                     class="pf-c-form-control" |  | ||||||
|                     required |  | ||||||
|                 /> |  | ||||||
|             </ak-form-element-horizontal>`; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| declare global { |  | ||||||
|     interface HTMLElementTagNameMap { |  | ||||||
|         "ak-policy-password-uniqueness-form": UniquePasswordPolicyForm; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 1.4 MiB After Width: | Height: | Size: 628 KiB | 
| @ -1,18 +1,13 @@ | |||||||
| /// <reference types="./types.js" />
 |  | ||||||
| 
 |  | ||||||
| /** | /** | ||||||
|  * @file Client-side observer for ESBuild events. |  * @file | ||||||
|  * |  * Client-side observer for ESBuild events. | ||||||
|  * @import { Message as ESBuildMessage } from "esbuild"; |  | ||||||
|  */ |  */ | ||||||
|  | import type { Message as ESBuildMessage } from "esbuild"; | ||||||
| 
 | 
 | ||||||
| const logPrefix = "👷 [ESBuild]"; | const logPrefix = "👷 [ESBuild]"; | ||||||
| const log = console.debug.bind(console, logPrefix); | const log = console.debug.bind(console, logPrefix); | ||||||
| 
 | 
 | ||||||
| /** | type BuildEventListener<Data = unknown> = (event: MessageEvent<Data>) => void; | ||||||
|  * @template {unknown} [Data=unknown] |  | ||||||
|  * @typedef {(event: MessageEvent) => void} BuildEventListener |  | ||||||
|  */ |  | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * A client-side watcher for ESBuild. |  * A client-side watcher for ESBuild. | ||||||
| @ -21,13 +16,14 @@ const log = console.debug.bind(console, logPrefix); | |||||||
|  * ESBuild may tree-shake it out of production builds. |  * ESBuild may tree-shake it out of production builds. | ||||||
|  * |  * | ||||||
|  * ```ts
 |  * ```ts
 | ||||||
|  * if (process.env.NODE_ENV === "development") { |  * if (process.env.NODE_ENV === "development" && process.env.WATCHER_URL) { | ||||||
|  *   await import("@goauthentik/esbuild-plugin-live-reload/client") |  *   const { ESBuildObserver } = await import("@goauthentik/common/client"); | ||||||
|  *     .catch(() => console.warn("Failed to import watcher")) |  * | ||||||
|  |  *   new ESBuildObserver(process.env.WATCHER_URL); | ||||||
|  * } |  * } | ||||||
|  * ``` |  * ``` | ||||||
|  * | } | ||||||
|  * @implements {Disposable} | 
 | ||||||
|  */ |  */ | ||||||
| export class ESBuildObserver extends EventSource { | export class ESBuildObserver extends EventSource { | ||||||
|     /** |     /** | ||||||
| @ -62,19 +58,15 @@ export class ESBuildObserver extends EventSource { | |||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * The interval for the keep-alive check. |      * The interval for the keep-alive check. | ||||||
|      * @type {ReturnType<typeof setInterval> | undefined} |  | ||||||
|      */ |      */ | ||||||
|     #keepAliveInterval; |     #keepAliveInterval: ReturnType<typeof setInterval> | undefined; | ||||||
| 
 | 
 | ||||||
|     #trackActivity = () => { |     #trackActivity = () => { | ||||||
|         this.lastUpdatedAt = Date.now(); |         this.lastUpdatedAt = Date.now(); | ||||||
|         this.alive = true; |         this.alive = true; | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     /** |     #startListener: BuildEventListener = () => { | ||||||
|      * @type {BuildEventListener} |  | ||||||
|      */ |  | ||||||
|     #startListener = () => { |  | ||||||
|         this.#trackActivity(); |         this.#trackActivity(); | ||||||
|         log("⏰  Build started..."); |         log("⏰  Build started..."); | ||||||
|     }; |     }; | ||||||
| @ -90,18 +82,13 @@ export class ESBuildObserver extends EventSource { | |||||||
|         } |         } | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     /** |     #errorListener: BuildEventListener<string> = (event) => { | ||||||
|      * @type {BuildEventListener<string>} |  | ||||||
|      */ |  | ||||||
|     #errorListener = (event) => { |  | ||||||
|         this.#trackActivity(); |         this.#trackActivity(); | ||||||
| 
 | 
 | ||||||
|  |         // eslint-disable-next-line no-console
 | ||||||
|         console.group(logPrefix, "⛔️⛔️⛔️  Build error..."); |         console.group(logPrefix, "⛔️⛔️⛔️  Build error..."); | ||||||
| 
 | 
 | ||||||
|         /** |         const esbuildErrorMessages: ESBuildMessage[] = JSON.parse(event.data); | ||||||
|          * @type {ESBuildMessage[]} |  | ||||||
|          */ |  | ||||||
|         const esbuildErrorMessages = JSON.parse(event.data); |  | ||||||
| 
 | 
 | ||||||
|         for (const error of esbuildErrorMessages) { |         for (const error of esbuildErrorMessages) { | ||||||
|             console.warn(error.text); |             console.warn(error.text); | ||||||
| @ -114,13 +101,11 @@ export class ESBuildObserver extends EventSource { | |||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |         // eslint-disable-next-line no-console
 | ||||||
|         console.groupEnd(); |         console.groupEnd(); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     /** |     #endListener: BuildEventListener = () => { | ||||||
|      * @type {BuildEventListener} |  | ||||||
|      */ |  | ||||||
|     #endListener = () => { |  | ||||||
|         cancelAnimationFrame(this.#reloadFrameID); |         cancelAnimationFrame(this.#reloadFrameID); | ||||||
| 
 | 
 | ||||||
|         this.#trackActivity(); |         this.#trackActivity(); | ||||||
| @ -141,36 +126,12 @@ export class ESBuildObserver extends EventSource { | |||||||
|         }); |         }); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     /** |     #keepAliveListener: BuildEventListener = () => { | ||||||
|      * @type {BuildEventListener} |  | ||||||
|      */ |  | ||||||
|     #keepAliveListener = () => { |  | ||||||
|         this.#trackActivity(); |         this.#trackActivity(); | ||||||
|         log("🏓 Keep-alive"); |         log("🏓 Keep-alive"); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     /** |     constructor(url: string | URL) { | ||||||
|      * Initialize the ESBuild observer. |  | ||||||
|      * This should be called once in your application. |  | ||||||
|      * |  | ||||||
|      * @param {string | URL} [url] |  | ||||||
|      * @returns {ESBuildObserver} |  | ||||||
|      */ |  | ||||||
|     static initialize = (url) => { |  | ||||||
|         const esbuildObserver = new ESBuildObserver(url); |  | ||||||
| 
 |  | ||||||
|         return esbuildObserver; |  | ||||||
|     }; |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * |  | ||||||
|      * @param {string | URL} [url] |  | ||||||
|      */ |  | ||||||
|     constructor(url) { |  | ||||||
|         if (!url) { |  | ||||||
|             throw new TypeError("ESBuildObserver: Cannot construct without a URL"); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         super(url); |         super(url); | ||||||
| 
 | 
 | ||||||
|         this.addEventListener("esbuild:start", this.#startListener); |         this.addEventListener("esbuild:start", this.#startListener); | ||||||
| @ -206,14 +167,4 @@ export class ESBuildObserver extends EventSource { | |||||||
|             log("👋  Waiting for build to start..."); |             log("👋  Waiting for build to start..."); | ||||||
|         }, 15_000); |         }, 15_000); | ||||||
|     } |     } | ||||||
| 
 |  | ||||||
|     [Symbol.dispose]() { |  | ||||||
|         return this.close(); |  | ||||||
| } | } | ||||||
| 
 |  | ||||||
|     dispose() { |  | ||||||
|         return this[Symbol.dispose](); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export default ESBuildObserver; |  | ||||||
| @ -1,110 +1,25 @@ | |||||||
| import type { Config as DOMPurifyConfig } from "dompurify"; |  | ||||||
| import DOMPurify from "dompurify"; | import DOMPurify from "dompurify"; | ||||||
| import { trustedTypes } from "trusted-types"; |  | ||||||
|  |  | ||||||
| import { render } from "lit"; | import { render } from "@lit-labs/ssr"; | ||||||
|  | import { collectResult } from "@lit-labs/ssr/lib/render-result.js"; | ||||||
|  | import { TemplateResult, html } from "lit"; | ||||||
| import { unsafeHTML } from "lit/directives/unsafe-html.js"; | import { unsafeHTML } from "lit/directives/unsafe-html.js"; | ||||||
|  | import { until } from "lit/directives/until.js"; | ||||||
|  |  | ||||||
| /** | export const DOM_PURIFY_STRICT: DOMPurify.Config = { | ||||||
|  * Trusted types policy that escapes HTML content in place. |  | ||||||
|  * |  | ||||||
|  * @see {@linkcode SanitizedTrustPolicy} to strip HTML content. |  | ||||||
|  * |  | ||||||
|  * @returns {TrustedHTML} All HTML content, escaped. |  | ||||||
|  */ |  | ||||||
| export const EscapeTrustPolicy = trustedTypes.createPolicy("authentik-escape", { |  | ||||||
|     createHTML: (untrustedHTML: string) => { |  | ||||||
|         return DOMPurify.sanitize(untrustedHTML, { |  | ||||||
|             RETURN_TRUSTED_TYPE: false, |  | ||||||
|         }); |  | ||||||
|     }, |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Trusted types policy, stripping all HTML content. |  | ||||||
|  * |  | ||||||
|  * @returns {TrustedHTML} Text content only, all HTML tags stripped. |  | ||||||
|  */ |  | ||||||
| export const SanitizedTrustPolicy = trustedTypes.createPolicy("authentik-sanitize", { |  | ||||||
|     createHTML: (untrustedHTML: string) => { |  | ||||||
|         return DOMPurify.sanitize(untrustedHTML, { |  | ||||||
|             RETURN_TRUSTED_TYPE: false, |  | ||||||
|     ALLOWED_TAGS: ["#text"], |     ALLOWED_TAGS: ["#text"], | ||||||
|         }); | }; | ||||||
|     }, |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| /** | export async function renderStatic(input: TemplateResult): Promise<string> { | ||||||
|  * Trusted types policy, allowing a minimal set of _safe_ HTML tags supplied by |     return await collectResult(render(input)); | ||||||
|  * a trusted source, such as the brand API. |  | ||||||
|  */ |  | ||||||
| export const BrandedHTMLPolicy = trustedTypes.createPolicy("authentik-restrict", { |  | ||||||
|     createHTML: (untrustedHTML: string) => { |  | ||||||
|         return DOMPurify.sanitize(untrustedHTML, { |  | ||||||
|             RETURN_TRUSTED_TYPE: false, |  | ||||||
|             FORBID_TAGS: [ |  | ||||||
|                 "script", |  | ||||||
|                 "style", |  | ||||||
|                 "iframe", |  | ||||||
|                 "link", |  | ||||||
|                 "object", |  | ||||||
|                 "embed", |  | ||||||
|                 "applet", |  | ||||||
|                 "meta", |  | ||||||
|                 "base", |  | ||||||
|                 "form", |  | ||||||
|                 "input", |  | ||||||
|                 "textarea", |  | ||||||
|                 "select", |  | ||||||
|                 "button", |  | ||||||
|             ], |  | ||||||
|             FORBID_ATTR: [ |  | ||||||
|                 "onerror", |  | ||||||
|                 "onclick", |  | ||||||
|                 "onload", |  | ||||||
|                 "onmouseover", |  | ||||||
|                 "onmouseout", |  | ||||||
|                 "onmouseup", |  | ||||||
|                 "onmousedown", |  | ||||||
|                 "onfocus", |  | ||||||
|                 "onblur", |  | ||||||
|                 "onsubmit", |  | ||||||
|             ], |  | ||||||
|         }); |  | ||||||
|     }, |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| export type AuthentikTrustPolicy = |  | ||||||
|     | typeof EscapeTrustPolicy |  | ||||||
|     | typeof SanitizedTrustPolicy |  | ||||||
|     | typeof BrandedHTMLPolicy; |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Sanitize an untrusted HTML string using a trusted types policy. |  | ||||||
|  */ |  | ||||||
| export function sanitizeHTML(trustPolicy: AuthentikTrustPolicy, untrustedHTML: string) { |  | ||||||
|     return unsafeHTML(trustPolicy.createHTML(untrustedHTML).toString()); |  | ||||||
| } | } | ||||||
|  |  | ||||||
| /** | export function purify(input: TemplateResult): TemplateResult { | ||||||
|  * DOMPurify configuration for strict sanitization. |     return html`${until( | ||||||
|  * |         (async () => { | ||||||
|  * This configuration only allows text nodes and disallows all HTML tags. |             const rendered = await renderStatic(input); | ||||||
|  */ |             const purified = DOMPurify.sanitize(rendered); | ||||||
| export const DOM_PURIFY_STRICT = { |             return html`${unsafeHTML(purified)}`; | ||||||
|     ALLOWED_TAGS: ["#text"], |         })(), | ||||||
| } as const satisfies DOMPurifyConfig; |     )}`; | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Render untrusted HTML to a string without escaping it. |  | ||||||
|  * |  | ||||||
|  * @returns {string} The rendered HTML string. |  | ||||||
|  */ |  | ||||||
| export function renderStaticHTMLUnsafe(untrustedHTML: unknown): string { |  | ||||||
|     const container = document.createElement("html"); |  | ||||||
|     render(untrustedHTML, container); |  | ||||||
|  |  | ||||||
|     const result = container.innerHTML; |  | ||||||
|  |  | ||||||
|     return result; |  | ||||||
| } | } | ||||||
|  | |||||||
| @ -17,16 +17,8 @@ | |||||||
|  |  | ||||||
|     /* Minimum width after which the sidebar becomes automatic */ |     /* Minimum width after which the sidebar becomes automatic */ | ||||||
|     --ak-sidebar--minimum-auto-width: 80rem; |     --ak-sidebar--minimum-auto-width: 80rem; | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * The height of the navbar and branded sidebar. |  | ||||||
|      * @todo This shouldn't be necessary. The sidebar can instead use a grid layout |  | ||||||
|      * ensuring they share the same height. |  | ||||||
|      */ |  | ||||||
|     --ak-navbar--height: 7rem; |  | ||||||
| } | } | ||||||
|  |  | ||||||
| @supports selector(::-webkit-scrollbar) { |  | ||||||
| ::-webkit-scrollbar { | ::-webkit-scrollbar { | ||||||
|     width: 5px; |     width: 5px; | ||||||
|     height: 5px; |     height: 5px; | ||||||
| @ -41,13 +33,6 @@ | |||||||
| ::-webkit-scrollbar-corner { | ::-webkit-scrollbar-corner { | ||||||
|     background-color: transparent; |     background-color: transparent; | ||||||
| } | } | ||||||
| } |  | ||||||
|  |  | ||||||
| @supports not selector(::-webkit-scrollbar) { |  | ||||||
|     :root { |  | ||||||
|         scrollbar-color: var(--ak-accent) transparent; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| html { | html { | ||||||
|     --pf-c-nav__link--PaddingTop: 0.5rem; |     --pf-c-nav__link--PaddingTop: 0.5rem; | ||||||
|  | |||||||
| @ -67,12 +67,6 @@ export class NavigationButtons extends AKElement { | |||||||
|                 :host([theme="light"]) .pf-c-page__header-tools-group .pf-c-button { |                 :host([theme="light"]) .pf-c-page__header-tools-group .pf-c-button { | ||||||
|                     color: var(--ak-global--Color--100) !important; |                     color: var(--ak-global--Color--100) !important; | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|                 @media (max-width: 768px) { |  | ||||||
|                     .pf-c-avatar { |  | ||||||
|                         display: none; |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             `, |             `, | ||||||
|         ]; |         ]; | ||||||
|     } |     } | ||||||
| @ -162,7 +156,9 @@ export class NavigationButtons extends AKElement { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     renderImpersonation() { |     renderImpersonation() { | ||||||
|         if (!this.me?.original) return nothing; |         if (!this.me?.original) { | ||||||
|  |             return nothing; | ||||||
|  |         } | ||||||
|  |  | ||||||
|         const onClick = async () => { |         const onClick = async () => { | ||||||
|             await new CoreApi(DEFAULT_CONFIG).coreUsersImpersonateEndRetrieve(); |             await new CoreApi(DEFAULT_CONFIG).coreUsersImpersonateEndRetrieve(); | ||||||
| @ -179,14 +175,6 @@ export class NavigationButtons extends AKElement { | |||||||
|             </div>`; |             </div>`; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     renderAvatar() { |  | ||||||
|         return html`<img |  | ||||||
|             class="pf-c-avatar" |  | ||||||
|             src=${ifDefined(this.me?.user.avatar)} |  | ||||||
|             alt="${msg("Avatar image")}" |  | ||||||
|         />`; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     get userDisplayName() { |     get userDisplayName() { | ||||||
|         return match<UserDisplay | undefined, string | undefined>(this.uiConfig?.navbar.userDisplay) |         return match<UserDisplay | undefined, string | undefined>(this.uiConfig?.navbar.userDisplay) | ||||||
|             .with(UserDisplay.username, () => this.me?.user.username) |             .with(UserDisplay.username, () => this.me?.user.username) | ||||||
| @ -224,7 +212,11 @@ export class NavigationButtons extends AKElement { | |||||||
|                       </div> |                       </div> | ||||||
|                   </div>` |                   </div>` | ||||||
|                 : nothing} |                 : nothing} | ||||||
|             ${this.renderAvatar()} |             <img | ||||||
|  |                 class="pf-c-avatar" | ||||||
|  |                 src=${ifDefined(this.me?.user.avatar)} | ||||||
|  |                 alt="${msg("Avatar image")}" | ||||||
|  |             /> | ||||||
|         </div>`; |         </div>`; | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,7 +1,7 @@ | |||||||
| import { authentikVersionContext } from "@goauthentik/elements/AuthentikContexts"; | import { authentikVersionContext } from "@goauthentik/elements/AuthentikContexts"; | ||||||
|  |  | ||||||
| import { consume } from "@lit/context"; | import { consume } from "@lit/context"; | ||||||
| import { Constructor } from "@lit/reactive-element/decorators/base.js"; | import { Constructor } from "@lit/reactive-element/decorators/base"; | ||||||
| import type { LitElement } from "lit"; | import type { LitElement } from "lit"; | ||||||
|  |  | ||||||
| import type { Version } from "@goauthentik/api"; | import type { Version } from "@goauthentik/api"; | ||||||
|  | |||||||
| @ -5,23 +5,20 @@ import { | |||||||
| } from "@goauthentik/common/constants"; | } from "@goauthentik/common/constants"; | ||||||
| import { globalAK } from "@goauthentik/common/global"; | import { globalAK } from "@goauthentik/common/global"; | ||||||
| import { currentInterface } from "@goauthentik/common/sentry"; | import { currentInterface } from "@goauthentik/common/sentry"; | ||||||
| import { UIConfig, UserDisplay, getConfigForUser } from "@goauthentik/common/ui/config"; | import { UIConfig, UserDisplay, uiConfig } from "@goauthentik/common/ui/config"; | ||||||
| import { me } from "@goauthentik/common/users"; | import { me } from "@goauthentik/common/users"; | ||||||
| import "@goauthentik/components/ak-nav-buttons"; | import "@goauthentik/components/ak-nav-buttons"; | ||||||
| import { AKElement } from "@goauthentik/elements/Base"; | import { AKElement } from "@goauthentik/elements/Base"; | ||||||
| import { WithBrandConfig } from "@goauthentik/elements/Interface/brandProvider"; | import { WithBrandConfig } from "@goauthentik/elements/Interface/brandProvider"; | ||||||
| import { DefaultBrand } from "@goauthentik/elements/sidebar/SidebarBrand"; |  | ||||||
| import { themeImage } from "@goauthentik/elements/utils/images"; |  | ||||||
| import "@patternfly/elements/pf-tooltip/pf-tooltip.js"; | import "@patternfly/elements/pf-tooltip/pf-tooltip.js"; | ||||||
|  |  | ||||||
| import { msg } from "@lit/localize"; | import { msg } from "@lit/localize"; | ||||||
| import { CSSResult, LitElement, TemplateResult, css, html, nothing } from "lit"; | import { CSSResult, TemplateResult, css, html, nothing } from "lit"; | ||||||
| import { customElement, property, state } from "lit/decorators.js"; | import { customElement, property, state } from "lit/decorators.js"; | ||||||
|  |  | ||||||
| import PFAvatar from "@patternfly/patternfly/components/Avatar/avatar.css"; | import PFAvatar from "@patternfly/patternfly/components/Avatar/avatar.css"; | ||||||
| import PFButton from "@patternfly/patternfly/components/Button/button.css"; | import PFButton from "@patternfly/patternfly/components/Button/button.css"; | ||||||
| import PFContent from "@patternfly/patternfly/components/Content/content.css"; | import PFContent from "@patternfly/patternfly/components/Content/content.css"; | ||||||
| import PFDrawer from "@patternfly/patternfly/components/Drawer/drawer.css"; |  | ||||||
| import PFDropdown from "@patternfly/patternfly/components/Dropdown/dropdown.css"; | import PFDropdown from "@patternfly/patternfly/components/Dropdown/dropdown.css"; | ||||||
| import PFNotificationBadge from "@patternfly/patternfly/components/NotificationBadge/notification-badge.css"; | import PFNotificationBadge from "@patternfly/patternfly/components/NotificationBadge/notification-badge.css"; | ||||||
| import PFPage from "@patternfly/patternfly/components/Page/page.css"; | import PFPage from "@patternfly/patternfly/components/Page/page.css"; | ||||||
| @ -29,52 +26,34 @@ import PFBase from "@patternfly/patternfly/patternfly-base.css"; | |||||||
|  |  | ||||||
| import { SessionUser } from "@goauthentik/api"; | import { SessionUser } from "@goauthentik/api"; | ||||||
|  |  | ||||||
| //#region Page Navbar | @customElement("ak-page-header") | ||||||
|  | export class PageHeader extends WithBrandConfig(AKElement) { | ||||||
| export interface PageNavbarDetails { |     @property() | ||||||
|     header?: string; |  | ||||||
|     description?: string; |  | ||||||
|     icon?: string; |     icon?: string; | ||||||
|     iconImage?: boolean; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /** |     @property({ type: Boolean }) | ||||||
|  * A global navbar component at the top of the page. |     iconImage = false; | ||||||
|  * |  | ||||||
|  * Internally, this component listens for the `ak-page-header` event, which is |  | ||||||
|  * dispatched by the `ak-page-header` component. |  | ||||||
|  */ |  | ||||||
| @customElement("ak-page-navbar") |  | ||||||
| export class AKPageNavbar extends WithBrandConfig(AKElement) implements PageNavbarDetails { |  | ||||||
|     //#region Static Properties |  | ||||||
|  |  | ||||||
|     private static elementRef: AKPageNavbar | null = null; |     @property() | ||||||
|  |     header = ""; | ||||||
|  |  | ||||||
|     static readonly setNavbarDetails = (detail: Partial<PageNavbarDetails>): void => { |     @property() | ||||||
|         const { elementRef } = AKPageNavbar; |     description?: string; | ||||||
|         if (!elementRef) { |  | ||||||
|             console.debug( |  | ||||||
|                 `ak-page-header: Could not find ak-page-navbar, skipping event dispatch.`, |  | ||||||
|             ); |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         const { header, description, icon, iconImage } = detail; |     @property({ type: Boolean }) | ||||||
|  |     hasIcon = true; | ||||||
|  |  | ||||||
|         elementRef.header = header; |     @state() | ||||||
|         elementRef.description = description; |     me?: SessionUser; | ||||||
|         elementRef.icon = icon; |  | ||||||
|         elementRef.iconImage = iconImage || false; |     @state() | ||||||
|         elementRef.hasIcon = !!icon; |     uiConfig!: UIConfig; | ||||||
|     }; |  | ||||||
|  |  | ||||||
|     static get styles(): CSSResult[] { |     static get styles(): CSSResult[] { | ||||||
|         return [ |         return [ | ||||||
|             PFBase, |             PFBase, | ||||||
|             PFButton, |             PFButton, | ||||||
|             PFPage, |             PFPage, | ||||||
|             PFDrawer, |  | ||||||
|  |  | ||||||
|             PFNotificationBadge, |             PFNotificationBadge, | ||||||
|             PFContent, |             PFContent, | ||||||
|             PFAvatar, |             PFAvatar, | ||||||
| @ -84,313 +63,127 @@ export class AKPageNavbar extends WithBrandConfig(AKElement) implements PageNavb | |||||||
|                     position: sticky; |                     position: sticky; | ||||||
|                     top: 0; |                     top: 0; | ||||||
|                     z-index: var(--pf-global--ZIndex--lg); |                     z-index: var(--pf-global--ZIndex--lg); | ||||||
|                     --pf-c-page__header-tools--MarginRight: 0; |  | ||||||
|                     --ak-brand-logo-height: var(--pf-global--FontSize--4xl, 2.25rem); |  | ||||||
|                     --ak-brand-background-color: var( |  | ||||||
|                         --pf-c-page__sidebar--m-light--BackgroundColor |  | ||||||
|                     ); |  | ||||||
|                 } |                 } | ||||||
|  |                 .bar { | ||||||
|                 :host([theme="dark"]) { |  | ||||||
|                     --ak-brand-background-color: var(--pf-c-page__sidebar--BackgroundColor); |  | ||||||
|                     --pf-c-page__sidebar--BackgroundColor: var(--ak-dark-background-light); |  | ||||||
|                     color: var(--ak-dark-foreground); |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 navbar { |  | ||||||
|                     border-bottom: var(--pf-global--BorderWidth--sm); |                     border-bottom: var(--pf-global--BorderWidth--sm); | ||||||
|                     border-bottom-style: solid; |                     border-bottom-style: solid; | ||||||
|                     border-bottom-color: var(--pf-global--BorderColor--100); |                     border-bottom-color: var(--pf-global--BorderColor--100); | ||||||
|                     background-color: var(--pf-c-page--BackgroundColor); |  | ||||||
|  |  | ||||||
|                     display: flex; |                     display: flex; | ||||||
|                     flex-direction: row; |                     flex-direction: row; | ||||||
|                     min-height: 6rem; |                     min-height: 114px; | ||||||
|  |                     max-height: 114px; | ||||||
|                     display: grid; |                     background-color: var(--pf-c-page--BackgroundColor); | ||||||
|                     row-gap: var(--pf-global--spacer--sm); |  | ||||||
|                     column-gap: var(--pf-global--spacer--sm); |  | ||||||
|                     grid-template-columns: [brand] auto [toggle] auto [primary] 1fr [secondary] auto; |  | ||||||
|                     grid-template-rows: auto auto; |  | ||||||
|                     grid-template-areas: |  | ||||||
|                         "brand toggle primary secondary" |  | ||||||
|                         "brand toggle description secondary"; |  | ||||||
|  |  | ||||||
|                     @media (max-width: 768px) { |  | ||||||
|                         row-gap: var(--pf-global--spacer--xs); |  | ||||||
|  |  | ||||||
|                         align-items: center; |  | ||||||
|                         grid-template-areas: |  | ||||||
|                             "toggle primary secondary" |  | ||||||
|                             "toggle description description"; |  | ||||||
|                         justify-content: space-between; |  | ||||||
|                         width: 100%; |  | ||||||
|                 } |                 } | ||||||
|  |                 .pf-c-page__main-section.pf-m-light { | ||||||
|  |                     background-color: transparent; | ||||||
|                 } |                 } | ||||||
|  |                 .pf-c-page__main-section { | ||||||
|                 .items { |                     flex-grow: 1; | ||||||
|                     display: block; |                     flex-shrink: 1; | ||||||
|  |  | ||||||
|                     &.primary { |  | ||||||
|                         grid-column: primary; |  | ||||||
|                         grid-row: primary / description; |  | ||||||
|  |  | ||||||
|                         align-content: center; |  | ||||||
|                         padding-block: var(--pf-global--spacer--md); |  | ||||||
|  |  | ||||||
|                         @media (min-width: 426px) { |  | ||||||
|                             &.block-sibling { |  | ||||||
|                                 padding-block-end: 0; |  | ||||||
|                                 grid-row: primary; |  | ||||||
|                             } |  | ||||||
|                         } |  | ||||||
|  |  | ||||||
|                         @media (max-width: 768px) { |  | ||||||
|                             padding-block: var(--pf-global--spacer--sm); |  | ||||||
|                         } |  | ||||||
|  |  | ||||||
|                         .accent-icon { |  | ||||||
|                             height: 1em; |  | ||||||
|                             width: 1em; |  | ||||||
|  |  | ||||||
|                             @media (max-width: 768px) { |  | ||||||
|                                 display: none; |  | ||||||
|                             } |  | ||||||
|                         } |  | ||||||
|                     } |  | ||||||
|  |  | ||||||
|                     &.page-description { |  | ||||||
|                         grid-area: description; |  | ||||||
|                         padding-block-end: var(--pf-global--spacer--md); |  | ||||||
|  |  | ||||||
|                         @media (max-width: 425px) { |  | ||||||
|                             display: none; |  | ||||||
|                         } |  | ||||||
|  |  | ||||||
|                         @media (min-width: 769px) { |  | ||||||
|                             text-wrap: balance; |  | ||||||
|                         } |  | ||||||
|                     } |  | ||||||
|  |  | ||||||
|                     &.secondary { |  | ||||||
|                         grid-area: secondary; |  | ||||||
|                         flex: 0 0 auto; |  | ||||||
|                         justify-self: end; |  | ||||||
|                         padding-block: var(--pf-global--spacer--sm); |  | ||||||
|                         padding-inline-end: var(--pf-global--spacer--sm); |  | ||||||
|  |  | ||||||
|                         @media (min-width: 769px) { |  | ||||||
|                             align-content: center; |  | ||||||
|                             padding-block: var(--pf-global--spacer--md); |  | ||||||
|                             padding-inline-end: var(--pf-global--spacer--xl); |  | ||||||
|                         } |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 .brand { |  | ||||||
|                     grid-area: brand; |  | ||||||
|                     background-color: var(--ak-brand-background-color); |  | ||||||
|                     height: 100%; |  | ||||||
|                     width: var(--pf-c-page__sidebar--Width); |  | ||||||
|                     align-items: center; |  | ||||||
|                     padding-inline: var(--pf-global--spacer--sm); |  | ||||||
|  |  | ||||||
|                     display: flex; |                     display: flex; | ||||||
|  |                     flex-direction: column; | ||||||
|                     justify-content: center; |                     justify-content: center; | ||||||
|  |  | ||||||
|                     &.pf-m-collapsed { |  | ||||||
|                         display: none; |  | ||||||
|                 } |                 } | ||||||
|  |                 img.pf-icon { | ||||||
|                     @media (max-width: 1279px) { |                     max-height: 24px; | ||||||
|                         display: none; |  | ||||||
|                 } |                 } | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 .sidebar-trigger { |  | ||||||
|                     grid-area: toggle; |  | ||||||
|                     height: 100%; |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 .logo { |  | ||||||
|                     flex: 0 0 auto; |  | ||||||
|                     height: var(--ak-brand-logo-height); |  | ||||||
|  |  | ||||||
|                     & img { |  | ||||||
|                         height: 100%; |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 .sidebar-trigger, |                 .sidebar-trigger, | ||||||
|                 .notification-trigger { |                 .notification-trigger { | ||||||
|                     font-size: 1.5rem; |                     font-size: 24px; | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|                 .notification-trigger.has-notifications { |                 .notification-trigger.has-notifications { | ||||||
|                     color: var(--pf-global--active-color--100); |                     color: var(--pf-global--active-color--100); | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|                 .page-title { |  | ||||||
|                     display: flex; |  | ||||||
|                     gap: var(--pf-global--spacer--xs); |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 h1 { |                 h1 { | ||||||
|                     display: flex; |                     display: flex; | ||||||
|                     flex-direction: row; |                     flex-direction: row; | ||||||
|                     align-items: center !important; |                     align-items: center !important; | ||||||
|                 } |                 } | ||||||
|  |                 .pf-c-page__header-tools { | ||||||
|  |                     flex-shrink: 0; | ||||||
|  |                 } | ||||||
|  |                 .pf-c-page__header-tools-group { | ||||||
|  |                     height: 100%; | ||||||
|  |                 } | ||||||
|  |                 :host([theme="dark"]) .pf-c-page__header-tools { | ||||||
|  |                     color: var(--ak-dark-foreground) !important; | ||||||
|  |                 } | ||||||
|             `, |             `, | ||||||
|         ]; |         ]; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     //#endregion |     constructor() { | ||||||
|  |         super(); | ||||||
|  |         window.addEventListener(EVENT_WS_MESSAGE, () => { | ||||||
|  |             this.firstUpdated(); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     //#region Properties |     async firstUpdated() { | ||||||
|  |         this.me = await me(); | ||||||
|  |         this.uiConfig = await uiConfig(); | ||||||
|  |         this.uiConfig.navbar.userDisplay = UserDisplay.none; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     @property({ type: String }) |     setTitle(header?: string) { | ||||||
|     icon?: string; |  | ||||||
|  |  | ||||||
|     @property({ type: Boolean }) |  | ||||||
|     iconImage = false; |  | ||||||
|  |  | ||||||
|     @property({ type: String }) |  | ||||||
|     header?: string; |  | ||||||
|  |  | ||||||
|     @property({ type: String }) |  | ||||||
|     description?: string; |  | ||||||
|  |  | ||||||
|     @property({ type: Boolean }) |  | ||||||
|     hasIcon = true; |  | ||||||
|  |  | ||||||
|     @property({ type: Boolean }) |  | ||||||
|     open = true; |  | ||||||
|  |  | ||||||
|     @state() |  | ||||||
|     session?: SessionUser; |  | ||||||
|  |  | ||||||
|     @state() |  | ||||||
|     uiConfig!: UIConfig; |  | ||||||
|  |  | ||||||
|     //#endregion |  | ||||||
|  |  | ||||||
|     //#region Private Methods |  | ||||||
|  |  | ||||||
|     #setTitle(header?: string) { |  | ||||||
|         const currentIf = currentInterface(); |         const currentIf = currentInterface(); | ||||||
|         let title = this.brand?.brandingTitle || TITLE_DEFAULT; |         let title = this.brand?.brandingTitle || TITLE_DEFAULT; | ||||||
|  |  | ||||||
|         if (currentIf === "admin") { |         if (currentIf === "admin") { | ||||||
|             title = `${msg("Admin")} - ${title}`; |             title = `${msg("Admin")} - ${title}`; | ||||||
|         } |         } | ||||||
|         // Prepend the header to the title |         // Prepend the header to the title | ||||||
|         if (header) { |         if (header !== undefined && header !== "") { | ||||||
|             title = `${header} - ${title}`; |             title = `${header} - ${title}`; | ||||||
|         } |         } | ||||||
|         document.title = title; |         document.title = title; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     #toggleSidebar() { |     willUpdate() { | ||||||
|         this.open = !this.open; |         // Always update title, even if there's no header value set, | ||||||
|  |         // as in that case we still need to return to the generic title | ||||||
|  |         this.setTitle(this.header); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     renderIcon() { | ||||||
|  |         if (this.icon) { | ||||||
|  |             if (this.iconImage && !this.icon.startsWith("fa://")) { | ||||||
|  |                 return html`<img class="pf-icon" src="${this.icon}" alt="page icon" />`; | ||||||
|  |             } | ||||||
|  |             const icon = this.icon.replaceAll("fa://", "fa "); | ||||||
|  |             return html`<i class=${icon}></i>`; | ||||||
|  |         } | ||||||
|  |         return nothing; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     render(): TemplateResult { | ||||||
|  |         return html`<div class="bar"> | ||||||
|  |             <button | ||||||
|  |                 class="sidebar-trigger pf-c-button pf-m-plain" | ||||||
|  |                 @click=${() => { | ||||||
|                     this.dispatchEvent( |                     this.dispatchEvent( | ||||||
|                         new CustomEvent(EVENT_SIDEBAR_TOGGLE, { |                         new CustomEvent(EVENT_SIDEBAR_TOGGLE, { | ||||||
|                             bubbles: true, |                             bubbles: true, | ||||||
|                             composed: true, |                             composed: true, | ||||||
|                         }), |                         }), | ||||||
|                     ); |                     ); | ||||||
|     } |                 }} | ||||||
|  |  | ||||||
|     //#endregion |  | ||||||
|  |  | ||||||
|     //#region Lifecycle |  | ||||||
|  |  | ||||||
|     public connectedCallback(): void { |  | ||||||
|         super.connectedCallback(); |  | ||||||
|         AKPageNavbar.elementRef = this; |  | ||||||
|  |  | ||||||
|         window.addEventListener(EVENT_WS_MESSAGE, () => { |  | ||||||
|             this.firstUpdated(); |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public disconnectedCallback(): void { |  | ||||||
|         super.disconnectedCallback(); |  | ||||||
|         AKPageNavbar.elementRef = null; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public async firstUpdated() { |  | ||||||
|         this.session = await me(); |  | ||||||
|         this.uiConfig = getConfigForUser(this.session.user); |  | ||||||
|         this.uiConfig.navbar.userDisplay = UserDisplay.none; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     willUpdate() { |  | ||||||
|         // Always update title, even if there's no header value set, |  | ||||||
|         // as in that case we still need to return to the generic title |  | ||||||
|         this.#setTitle(this.header); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     //#endregion |  | ||||||
|  |  | ||||||
|     //#region Render |  | ||||||
|  |  | ||||||
|     renderIcon() { |  | ||||||
|         if (this.icon) { |  | ||||||
|             if (this.iconImage && !this.icon.startsWith("fa://")) { |  | ||||||
|                 return html`<img class="accent-icon pf-icon" src="${this.icon}" alt="page icon" />`; |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             const icon = this.icon.replaceAll("fa://", "fa "); |  | ||||||
|  |  | ||||||
|             return html`<i class="accent-icon ${icon}"></i>`; |  | ||||||
|         } |  | ||||||
|         return nothing; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     render(): TemplateResult { |  | ||||||
|         return html`<navbar aria-label="Main" class="navbar"> |  | ||||||
|                 <aside class="brand ${this.open ? "" : "pf-m-collapsed"}"> |  | ||||||
|                     <a href="#/"> |  | ||||||
|                         <div class="logo"> |  | ||||||
|                             <img |  | ||||||
|                                 src=${themeImage( |  | ||||||
|                                     this.brand?.brandingLogo ?? DefaultBrand.brandingLogo, |  | ||||||
|                                 )} |  | ||||||
|                                 alt="${msg("authentik Logo")}" |  | ||||||
|                                 loading="lazy" |  | ||||||
|                             /> |  | ||||||
|                         </div> |  | ||||||
|                     </a> |  | ||||||
|                 </aside> |  | ||||||
|                 <button |  | ||||||
|                     class="sidebar-trigger pf-c-button pf-m-plain" |  | ||||||
|                     @click=${this.#toggleSidebar} |  | ||||||
|                     aria-label=${msg("Toggle sidebar")} |  | ||||||
|                     aria-expanded=${this.open ? "true" : "false"} |  | ||||||
|             > |             > | ||||||
|                 <i class="fas fa-bars"></i> |                 <i class="fas fa-bars"></i> | ||||||
|             </button> |             </button> | ||||||
|  |             <section class="pf-c-page__main-section pf-m-light"> | ||||||
|                 <section |                 <div class="pf-c-content"> | ||||||
|                     class="items primary pf-c-content ${this.description ? "block-sibling" : ""}" |                     <h1> | ||||||
|                 > |  | ||||||
|                     <h1 class="page-title"> |  | ||||||
|                         ${this.hasIcon |                         ${this.hasIcon | ||||||
|                             ? html`<slot name="icon">${this.renderIcon()}</slot>` |                             ? html`<slot name="icon">${this.renderIcon()}</slot> ` | ||||||
|                             : nothing} |                             : nothing} | ||||||
|                         ${this.header} |                         <slot name="header">${this.header}</slot> | ||||||
|                     </h1> |                     </h1> | ||||||
|  |                     ${this.description ? html`<p>${this.description}</p>` : html``} | ||||||
|  |                 </div> | ||||||
|             </section> |             </section> | ||||||
|                 ${this.description |             <div class="pf-c-page__header-tools"> | ||||||
|                     ? html`<section class="items page-description pf-c-content"> |  | ||||||
|                           <p>${this.description}</p> |  | ||||||
|                       </section>` |  | ||||||
|                     : nothing} |  | ||||||
|  |  | ||||||
|                 <section class="items secondary"> |  | ||||||
|                 <div class="pf-c-page__header-tools-group"> |                 <div class="pf-c-page__header-tools-group"> | ||||||
|                         <ak-nav-buttons .uiConfig=${this.uiConfig} .me=${this.session}> |                     <ak-nav-buttons .uiConfig=${this.uiConfig} .me=${this.me}> | ||||||
|                         <a |                         <a | ||||||
|                             class="pf-c-button pf-m-secondary pf-m-small pf-u-display-none pf-u-display-block-on-md" |                             class="pf-c-button pf-m-secondary pf-m-small pf-u-display-none pf-u-display-block-on-md" | ||||||
|                             href="${globalAK().api.base}if/user/" |                             href="${globalAK().api.base}if/user/" | ||||||
| @ -400,76 +193,13 @@ export class AKPageNavbar extends WithBrandConfig(AKElement) implements PageNavb | |||||||
|                         </a> |                         </a> | ||||||
|                     </ak-nav-buttons> |                     </ak-nav-buttons> | ||||||
|                 </div> |                 </div> | ||||||
|                 </section> |             </div> | ||||||
|             </navbar> |         </div>`; | ||||||
|             <slot></slot>`; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     //#endregion |  | ||||||
| } |  | ||||||
|  |  | ||||||
| //#endregion |  | ||||||
|  |  | ||||||
| //#region Page Header |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * A page header component, used to display the page title and description. |  | ||||||
|  * |  | ||||||
|  * Internally, this component dispatches the `ak-page-header` event, which is |  | ||||||
|  * listened to by the `ak-page-navbar` component. |  | ||||||
|  * |  | ||||||
|  * @singleton |  | ||||||
|  */ |  | ||||||
| @customElement("ak-page-header") |  | ||||||
| export class AKPageHeader extends LitElement implements PageNavbarDetails { |  | ||||||
|     @property({ type: String }) |  | ||||||
|     header?: string; |  | ||||||
|  |  | ||||||
|     @property({ type: String }) |  | ||||||
|     description?: string; |  | ||||||
|  |  | ||||||
|     @property({ type: String }) |  | ||||||
|     icon?: string; |  | ||||||
|  |  | ||||||
|     @property({ type: Boolean }) |  | ||||||
|     iconImage = false; |  | ||||||
|  |  | ||||||
|     static get styles(): CSSResult[] { |  | ||||||
|         return [ |  | ||||||
|             css` |  | ||||||
|                 :host { |  | ||||||
|                     display: none; |  | ||||||
|                 } |  | ||||||
|             `, |  | ||||||
|         ]; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     connectedCallback(): void { |  | ||||||
|         super.connectedCallback(); |  | ||||||
|  |  | ||||||
|         AKPageNavbar.setNavbarDetails({ |  | ||||||
|             header: this.header, |  | ||||||
|             description: this.description, |  | ||||||
|             icon: this.icon, |  | ||||||
|             iconImage: this.iconImage, |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     updated(): void { |  | ||||||
|         AKPageNavbar.setNavbarDetails({ |  | ||||||
|             header: this.header, |  | ||||||
|             description: this.description, |  | ||||||
|             icon: this.icon, |  | ||||||
|             iconImage: this.iconImage, |  | ||||||
|         }); |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| //#endregion |  | ||||||
|  |  | ||||||
| declare global { | declare global { | ||||||
|     interface HTMLElementTagNameMap { |     interface HTMLElementTagNameMap { | ||||||
|         "ak-page-header": AKPageHeader; |         "ak-page-header": PageHeader; | ||||||
|         "ak-page-navbar": AKPageNavbar; |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -2,7 +2,7 @@ import { AkControlElement } from "@goauthentik/elements/AkControlElement"; | |||||||
| import { CustomEmitterElement } from "@goauthentik/elements/utils/eventEmitter"; | import { CustomEmitterElement } from "@goauthentik/elements/utils/eventEmitter"; | ||||||
|  |  | ||||||
| import { msg } from "@lit/localize"; | import { msg } from "@lit/localize"; | ||||||
| import { PropertyValues } from "@lit/reactive-element"; | import { PropertyValues } from "@lit/reactive-element/reactive-element"; | ||||||
| import { TemplateResult, css, html } from "lit"; | import { TemplateResult, css, html } from "lit"; | ||||||
| import { customElement, property, queryAll, state } from "lit/decorators.js"; | import { customElement, property, queryAll, state } from "lit/decorators.js"; | ||||||
| import { map } from "lit/directives/map.js"; | import { map } from "lit/directives/map.js"; | ||||||
|  | |||||||
| @ -80,14 +80,13 @@ export class AggregateCard extends AKElement implements IAggregateCard { | |||||||
|                 .center-value { |                 .center-value { | ||||||
|                     font-size: var(--pf-global--icon--FontSize--lg); |                     font-size: var(--pf-global--icon--FontSize--lg); | ||||||
|                     text-align: center; |                     text-align: center; | ||||||
|                     place-content: center; |  | ||||||
|                 } |                 } | ||||||
|                 .subtext { |                 .subtext { | ||||||
|                     margin-top: var(--pf-global--spacer--sm); |                     margin-top: var(--pf-global--spacer--sm); | ||||||
|                     font-size: var(--pf-global--FontSize--sm); |                     font-size: var(--pf-global--FontSize--sm); | ||||||
|                 } |                 } | ||||||
|                 .pf-c-card__body { |                 .pf-c-card__body { | ||||||
|                     overflow-x: auto; |                     overflow-x: scroll; | ||||||
|                     padding-left: calc(var(--pf-c-card--child--PaddingLeft) / 2); |                     padding-left: calc(var(--pf-c-card--child--PaddingLeft) / 2); | ||||||
|                     padding-right: calc(var(--pf-c-card--child--PaddingRight) / 2); |                     padding-right: calc(var(--pf-c-card--child--PaddingRight) / 2); | ||||||
|                 } |                 } | ||||||
|  | |||||||
| @ -35,7 +35,10 @@ export class Sidebar extends AKElement { | |||||||
|                 .pf-c-nav__section + .pf-c-nav__section { |                 .pf-c-nav__section + .pf-c-nav__section { | ||||||
|                     --pf-c-nav__section--section--MarginTop: var(--pf-global--spacer--sm); |                     --pf-c-nav__section--section--MarginTop: var(--pf-global--spacer--sm); | ||||||
|                 } |                 } | ||||||
|  |                 .pf-c-nav__list .sidebar-brand { | ||||||
|  |                     max-height: 82px; | ||||||
|  |                     margin-bottom: -0.5rem; | ||||||
|  |                 } | ||||||
|                 nav { |                 nav { | ||||||
|                     display: flex; |                     display: flex; | ||||||
|                     flex-direction: column; |                     flex-direction: column; | ||||||
| @ -67,6 +70,7 @@ export class Sidebar extends AKElement { | |||||||
|             class="pf-c-nav ${this.activeTheme === UiThemeEnum.Light ? "pf-m-light" : ""}" |             class="pf-c-nav ${this.activeTheme === UiThemeEnum.Light ? "pf-m-light" : ""}" | ||||||
|             aria-label=${msg("Global")} |             aria-label=${msg("Global")} | ||||||
|         > |         > | ||||||
|  |             <ak-sidebar-brand></ak-sidebar-brand> | ||||||
|             <ul class="pf-c-nav__list"> |             <ul class="pf-c-nav__list"> | ||||||
|                 <slot></slot> |                 <slot></slot> | ||||||
|             </ul> |             </ul> | ||||||
|  | |||||||
| @ -1,3 +1,17 @@ | |||||||
|  | import { EVENT_SIDEBAR_TOGGLE } from "@goauthentik/common/constants"; | ||||||
|  | import { AKElement } from "@goauthentik/elements/Base"; | ||||||
|  | import { WithBrandConfig } from "@goauthentik/elements/Interface/brandProvider"; | ||||||
|  | import { themeImage } from "@goauthentik/elements/utils/images"; | ||||||
|  |  | ||||||
|  | import { msg } from "@lit/localize"; | ||||||
|  | import { CSSResult, TemplateResult, css, html } from "lit"; | ||||||
|  | import { customElement } from "lit/decorators.js"; | ||||||
|  |  | ||||||
|  | import PFButton from "@patternfly/patternfly/components/Button/button.css"; | ||||||
|  | import PFPage from "@patternfly/patternfly/components/Page/page.css"; | ||||||
|  | import PFGlobal from "@patternfly/patternfly/patternfly-base.css"; | ||||||
|  | import PFBase from "@patternfly/patternfly/patternfly-base.css"; | ||||||
|  |  | ||||||
| import { CurrentBrand, UiThemeEnum } from "@goauthentik/api"; | import { CurrentBrand, UiThemeEnum } from "@goauthentik/api"; | ||||||
|  |  | ||||||
| // If the viewport is wider than MIN_WIDTH, the sidebar | // If the viewport is wider than MIN_WIDTH, the sidebar | ||||||
| @ -14,3 +28,79 @@ export const DefaultBrand: CurrentBrand = { | |||||||
|     matchedDomain: "", |     matchedDomain: "", | ||||||
|     defaultLocale: "", |     defaultLocale: "", | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | @customElement("ak-sidebar-brand") | ||||||
|  | export class SidebarBrand extends WithBrandConfig(AKElement) { | ||||||
|  |     static get styles(): CSSResult[] { | ||||||
|  |         return [ | ||||||
|  |             PFBase, | ||||||
|  |             PFGlobal, | ||||||
|  |             PFPage, | ||||||
|  |             PFButton, | ||||||
|  |             css` | ||||||
|  |                 :host { | ||||||
|  |                     display: flex; | ||||||
|  |                     flex-direction: row; | ||||||
|  |                     align-items: center; | ||||||
|  |                     height: 114px; | ||||||
|  |                     min-height: 114px; | ||||||
|  |                     border-bottom: var(--pf-global--BorderWidth--sm); | ||||||
|  |                     border-bottom-style: solid; | ||||||
|  |                     border-bottom-color: var(--pf-global--BorderColor--100); | ||||||
|  |                 } | ||||||
|  |                 .pf-c-brand img { | ||||||
|  |                     padding: 0 0.5rem; | ||||||
|  |                     height: 42px; | ||||||
|  |                 } | ||||||
|  |                 button.pf-c-button.sidebar-trigger { | ||||||
|  |                     background-color: transparent; | ||||||
|  |                     border-radius: 0px; | ||||||
|  |                     height: 100%; | ||||||
|  |                     color: var(--ak-dark-foreground); | ||||||
|  |                 } | ||||||
|  |             `, | ||||||
|  |         ]; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     constructor() { | ||||||
|  |         super(); | ||||||
|  |         window.addEventListener("resize", () => { | ||||||
|  |             this.requestUpdate(); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     render(): TemplateResult { | ||||||
|  |         return html` ${window.innerWidth <= MIN_WIDTH | ||||||
|  |                 ? html` | ||||||
|  |                       <button | ||||||
|  |                           class="sidebar-trigger pf-c-button" | ||||||
|  |                           @click=${() => { | ||||||
|  |                               this.dispatchEvent( | ||||||
|  |                                   new CustomEvent(EVENT_SIDEBAR_TOGGLE, { | ||||||
|  |                                       bubbles: true, | ||||||
|  |                                       composed: true, | ||||||
|  |                                   }), | ||||||
|  |                               ); | ||||||
|  |                           }} | ||||||
|  |                       > | ||||||
|  |                           <i class="fas fa-bars"></i> | ||||||
|  |                       </button> | ||||||
|  |                   ` | ||||||
|  |                 : html``} | ||||||
|  |             <a href="#/" class="pf-c-page__header-brand-link"> | ||||||
|  |                 <div class="pf-c-brand ak-brand"> | ||||||
|  |                     <img | ||||||
|  |                         src=${themeImage(this.brand?.brandingLogo ?? DefaultBrand.brandingLogo)} | ||||||
|  |                         alt="${msg("authentik Logo")}" | ||||||
|  |                         loading="lazy" | ||||||
|  |                     /> | ||||||
|  |                 </div> | ||||||
|  |             </a>`; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | declare global { | ||||||
|  |     interface HTMLElementTagNameMap { | ||||||
|  |         "ak-sidebar-brand": SidebarBrand; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | |||||||
| @ -1,55 +0,0 @@ | |||||||
| /** |  | ||||||
|  * @file IFrame Utilities |  | ||||||
|  */ |  | ||||||
|  |  | ||||||
| interface IFrameLoadResult { |  | ||||||
|     contentWindow: Window; |  | ||||||
|     contentDocument: Document; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| export function pluckIFrameContent(iframe: HTMLIFrameElement) { |  | ||||||
|     const contentWindow = iframe.contentWindow; |  | ||||||
|     const contentDocument = iframe.contentDocument; |  | ||||||
|  |  | ||||||
|     if (!contentWindow) { |  | ||||||
|         throw new Error("Iframe contentWindow is not accessible"); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     if (!contentDocument) { |  | ||||||
|         throw new Error("Iframe contentDocument is not accessible"); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     return { |  | ||||||
|         contentWindow, |  | ||||||
|         contentDocument, |  | ||||||
|     }; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| export function resolveIFrameContent(iframe: HTMLIFrameElement): Promise<IFrameLoadResult> { |  | ||||||
|     if (iframe.contentDocument?.readyState === "complete") { |  | ||||||
|         return Promise.resolve(pluckIFrameContent(iframe)); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     return new Promise((resolve) => { |  | ||||||
|         iframe.addEventListener("load", () => resolve(pluckIFrameContent(iframe)), { once: true }); |  | ||||||
|     }); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Creates a minimal HTML wrapper for an iframe. |  | ||||||
|  * |  | ||||||
|  * @deprecated Use the `contentDocument.body` directly instead. |  | ||||||
|  */ |  | ||||||
| export function createIFrameHTMLWrapper(bodyContent: string): string { |  | ||||||
|     const html = String.raw; |  | ||||||
|  |  | ||||||
|     return html`<!doctype html> |  | ||||||
|         <html> |  | ||||||
|             <head> |  | ||||||
|                 <meta charset="utf-8" /> |  | ||||||
|             </head> |  | ||||||
|             <body style="display:flex;flex-direction:row;justify-content:center;"> |  | ||||||
|                 ${bodyContent} |  | ||||||
|             </body> |  | ||||||
|         </html>`; |  | ||||||
| } |  | ||||||
| @ -14,6 +14,8 @@ import "@goauthentik/flow/stages/password/PasswordStage"; | |||||||
|  |  | ||||||
| // end of stage import | // end of stage import | ||||||
|  |  | ||||||
| if (process.env.NODE_ENV === "development") { | if (process.env.NODE_ENV === "development" && process.env.WATCHER_URL) { | ||||||
|     await import("@goauthentik/esbuild-plugin-live-reload/client"); |     const { ESBuildObserver } = await import("@goauthentik/common/client"); | ||||||
|  |  | ||||||
|  |     new ESBuildObserver(process.env.WATCHER_URL); | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,4 +1,4 @@ | |||||||
| import { BrandedHTMLPolicy, sanitizeHTML } from "@goauthentik/common/purify"; | import { purify } from "@goauthentik/common/purify"; | ||||||
| import { AKElement } from "@goauthentik/elements/Base.js"; | import { AKElement } from "@goauthentik/elements/Base.js"; | ||||||
|  |  | ||||||
| import { msg } from "@lit/localize"; | import { msg } from "@lit/localize"; | ||||||
| @ -21,6 +21,8 @@ const styles = css` | |||||||
|     } |     } | ||||||
| `; | `; | ||||||
|  |  | ||||||
|  | const poweredBy: FooterLink = { name: msg("Powered by authentik"), href: null }; | ||||||
|  |  | ||||||
| @customElement("ak-brand-links") | @customElement("ak-brand-links") | ||||||
| export class BrandLinks extends AKElement { | export class BrandLinks extends AKElement { | ||||||
|     static get styles() { |     static get styles() { | ||||||
| @ -31,21 +33,13 @@ export class BrandLinks extends AKElement { | |||||||
|     links: FooterLink[] = []; |     links: FooterLink[] = []; | ||||||
|  |  | ||||||
|     render() { |     render() { | ||||||
|         const links = [...(this.links ?? [])]; |         const links = [...(this.links ?? []), poweredBy]; | ||||||
|  |  | ||||||
|         return html` <ul class="pf-c-list pf-m-inline"> |         return html` <ul class="pf-c-list pf-m-inline"> | ||||||
|             ${map(links, (link) => { |             ${map(links, (link) => | ||||||
|                 const children = sanitizeHTML(BrandedHTMLPolicy, link.name); |                 link.href | ||||||
|  |                     ? purify(html`<li><a href="${link.href}">${link.name}</a></li>`) | ||||||
|                 if (link.href) { |                     : html`<li><span>${link.name}</span></li>`, | ||||||
|                     return html`<li><a href="${link.href}">${children}</a></li>`; |             )} | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 return html`<li> |  | ||||||
|                     <span> ${children} </span> |  | ||||||
|                 </li>`; |  | ||||||
|             })} |  | ||||||
|             <li><span>${msg("Powered by authentik")}</span></li> |  | ||||||
|         </ul>`; |         </ul>`; | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,16 +1,15 @@ | |||||||
| ///<reference types="@hcaptcha/types"/> | ///<reference types="@hcaptcha/types"/> | ||||||
| /// <reference types="turnstile-types"/> | import { renderStatic } from "@goauthentik/common/purify"; | ||||||
| import { renderStaticHTMLUnsafe } from "@goauthentik/common/purify"; |  | ||||||
| import "@goauthentik/elements/EmptyState"; | import "@goauthentik/elements/EmptyState"; | ||||||
| import { akEmptyState } from "@goauthentik/elements/EmptyState"; | import { akEmptyState } from "@goauthentik/elements/EmptyState"; | ||||||
| import { bound } from "@goauthentik/elements/decorators/bound"; | import { bound } from "@goauthentik/elements/decorators/bound"; | ||||||
| import "@goauthentik/elements/forms/FormElement"; | import "@goauthentik/elements/forms/FormElement"; | ||||||
| import { createIFrameHTMLWrapper } from "@goauthentik/elements/utils/iframe"; |  | ||||||
| import { ListenerController } from "@goauthentik/elements/utils/listenerController.js"; | import { ListenerController } from "@goauthentik/elements/utils/listenerController.js"; | ||||||
| import { randomId } from "@goauthentik/elements/utils/randomId"; | import { randomId } from "@goauthentik/elements/utils/randomId"; | ||||||
| import "@goauthentik/flow/FormStatic"; | import "@goauthentik/flow/FormStatic"; | ||||||
| import { BaseStage } from "@goauthentik/flow/stages/base"; | import { BaseStage } from "@goauthentik/flow/stages/base"; | ||||||
| import { P, match } from "ts-pattern"; | import { P, match } from "ts-pattern"; | ||||||
|  | import type * as _ from "turnstile-types"; | ||||||
|  |  | ||||||
| import { msg } from "@lit/localize"; | import { msg } from "@lit/localize"; | ||||||
| import { CSSResult, PropertyValues, TemplateResult, css, html, nothing } from "lit"; | import { CSSResult, PropertyValues, TemplateResult, css, html, nothing } from "lit"; | ||||||
| @ -57,14 +56,18 @@ type CaptchaHandler = { | |||||||
| // a resize. Because the Captcha is itself in an iframe, the reported height is often off by some | // a resize. Because the Captcha is itself in an iframe, the reported height is often off by some | ||||||
| // margin, so adding 2rem of height to our container adds padding and prevents scroll bars or hidden | // margin, so adding 2rem of height to our container adds padding and prevents scroll bars or hidden | ||||||
| // rendering. | // rendering. | ||||||
| function iframeTemplate(children: TemplateResult, challengeURL: string): TemplateResult { |  | ||||||
|     return html` ${children} | const iframeTemplate = (captchaElement: TemplateResult, challengeUrl: string) => | ||||||
|  |     html`<!doctype html> | ||||||
|  |         <head> | ||||||
|  |             <html> | ||||||
|  |                 <body style="display:flex;flex-direction:row;justify-content:center;"> | ||||||
|  |                     ${captchaElement} | ||||||
|                     <script> |                     <script> | ||||||
|                         new ResizeObserver((entries) => { |                         new ResizeObserver((entries) => { | ||||||
|                             const height = |                             const height = | ||||||
|                                 document.body.offsetHeight + |                                 document.body.offsetHeight + | ||||||
|                                 parseFloat(getComputedStyle(document.body).fontSize) * 2; |                                 parseFloat(getComputedStyle(document.body).fontSize) * 2; | ||||||
|  |  | ||||||
|                             window.parent.postMessage({ |                             window.parent.postMessage({ | ||||||
|                                 message: "resize", |                                 message: "resize", | ||||||
|                                 source: "goauthentik.io", |                                 source: "goauthentik.io", | ||||||
| @ -73,20 +76,20 @@ function iframeTemplate(children: TemplateResult, challengeURL: string): Templat | |||||||
|                             }); |                             }); | ||||||
|                         }).observe(document.querySelector(".ak-captcha-container")); |                         }).observe(document.querySelector(".ak-captcha-container")); | ||||||
|                     </script> |                     </script> | ||||||
|  |                     <script src=${challengeUrl}></script> | ||||||
|         <script src=${challengeURL}></script> |  | ||||||
|  |  | ||||||
|                     <script> |                     <script> | ||||||
|                         function callback(token) { |                         function callback(token) { | ||||||
|                             window.parent.postMessage({ |                             window.parent.postMessage({ | ||||||
|                                 message: "captcha", |                                 message: "captcha", | ||||||
|                                 source: "goauthentik.io", |                                 source: "goauthentik.io", | ||||||
|                                 context: "flow-executor", |                                 context: "flow-executor", | ||||||
|                     token, |                                 token: token, | ||||||
|                             }); |                             }); | ||||||
|                         } |                         } | ||||||
|         </script>`; |                     </script> | ||||||
| } |                 </body> | ||||||
|  |             </html> | ||||||
|  |         </head>`; | ||||||
|  |  | ||||||
| @customElement("ak-stage-captcha") | @customElement("ak-stage-captcha") | ||||||
| export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeResponseRequest> { | export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeResponseRequest> { | ||||||
| @ -302,25 +305,11 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     async renderFrame(captchaElement: TemplateResult) { |     async renderFrame(captchaElement: TemplateResult) { | ||||||
|         const { contentDocument } = this.captchaFrame || {}; |         this.captchaFrame.contentWindow?.document.open(); | ||||||
|  |         this.captchaFrame.contentWindow?.document.write( | ||||||
|         if (!contentDocument) { |             await renderStatic(iframeTemplate(captchaElement, this.challenge.jsUrl)), | ||||||
|             console.debug( |  | ||||||
|                 "authentik/stages/captcha: unable to render captcha frame, no contentDocument", |  | ||||||
|         ); |         ); | ||||||
|  |         this.captchaFrame.contentWindow?.document.close(); | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         contentDocument.open(); |  | ||||||
|  |  | ||||||
|         contentDocument.write( |  | ||||||
|             createIFrameHTMLWrapper( |  | ||||||
|                 renderStaticHTMLUnsafe(iframeTemplate(captchaElement, this.challenge.jsUrl)), |  | ||||||
|             ), |  | ||||||
|         ); |  | ||||||
|  |  | ||||||
|         contentDocument.close(); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     renderBody() { |     renderBody() { | ||||||
|  | |||||||
| @ -43,10 +43,6 @@ import PFDisplay from "@patternfly/patternfly/utilities/Display/display.css"; | |||||||
|  |  | ||||||
| import { CurrentBrand, EventsApi, SessionUser } from "@goauthentik/api"; | import { CurrentBrand, EventsApi, SessionUser } from "@goauthentik/api"; | ||||||
|  |  | ||||||
| if (process.env.NODE_ENV === "development") { |  | ||||||
|     await import("@goauthentik/esbuild-plugin-live-reload/client"); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| const customStyles = css` | const customStyles = css` | ||||||
|     .pf-c-page__main, |     .pf-c-page__main, | ||||||
|     .pf-c-drawer__content, |     .pf-c-drawer__content, | ||||||
| @ -295,6 +291,12 @@ export class UserInterface extends AuthenticatedInterface { | |||||||
|         window.addEventListener(EVENT_NOTIFICATION_DRAWER_TOGGLE, this.toggleNotificationDrawer); |         window.addEventListener(EVENT_NOTIFICATION_DRAWER_TOGGLE, this.toggleNotificationDrawer); | ||||||
|         window.addEventListener(EVENT_API_DRAWER_TOGGLE, this.toggleApiDrawer); |         window.addEventListener(EVENT_API_DRAWER_TOGGLE, this.toggleApiDrawer); | ||||||
|         window.addEventListener(EVENT_WS_MESSAGE, this.fetchConfigurationDetails); |         window.addEventListener(EVENT_WS_MESSAGE, this.fetchConfigurationDetails); | ||||||
|  |  | ||||||
|  |         if (process.env.NODE_ENV === "development" && process.env.WATCHER_URL) { | ||||||
|  |             const { ESBuildObserver } = await import("@goauthentik/common/client"); | ||||||
|  |  | ||||||
|  |             new ESBuildObserver(process.env.WATCHER_URL); | ||||||
|  |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     disconnectedCallback() { |     disconnectedCallback() { | ||||||
|  | |||||||
							
								
								
									
										71
									
								
								web/tsconfig.base.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								web/tsconfig.base.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,71 @@ | |||||||
|  | { | ||||||
|  |     "compilerOptions": { | ||||||
|  |         "strict": true, | ||||||
|  |         "baseUrl": ".", | ||||||
|  |         "esModuleInterop": true, | ||||||
|  |         "paths": { | ||||||
|  |             "@goauthentik/docs/*": ["../website/docs/*"] | ||||||
|  |         }, | ||||||
|  |         "types": [ | ||||||
|  |             "node", | ||||||
|  |             "@wdio/mocha-framework", | ||||||
|  |             "@wdio/types", | ||||||
|  |             "expect-webdriverio", | ||||||
|  |             "grecaptcha" | ||||||
|  |         ], | ||||||
|  |         "jsx": "react-jsx", | ||||||
|  |         "skipLibCheck": true, | ||||||
|  |         "forceConsistentCasingInFileNames": true, | ||||||
|  |         "experimentalDecorators": true, | ||||||
|  |         "sourceMap": true, | ||||||
|  |         "target": "esnext", | ||||||
|  |         "module": "esnext", | ||||||
|  |         "moduleResolution": "node", | ||||||
|  |         "lib": [ | ||||||
|  |             "ES5", | ||||||
|  |             "ES2015", | ||||||
|  |             "ES2016", | ||||||
|  |             "ES2017", | ||||||
|  |             "ES2018", | ||||||
|  |             "ES2019", | ||||||
|  |             "ES2020", | ||||||
|  |             "ESNext", | ||||||
|  |             "DOM", | ||||||
|  |             "DOM.Iterable", | ||||||
|  |             "WebWorker" | ||||||
|  |         ], | ||||||
|  |         "noUnusedLocals": true, | ||||||
|  |         "noImplicitReturns": true, | ||||||
|  |         "noFallthroughCasesInSwitch": true, | ||||||
|  |         "strictBindCallApply": true, | ||||||
|  |         "strictFunctionTypes": true, | ||||||
|  |         "strictNullChecks": true, | ||||||
|  |         "allowUnreachableCode": false, | ||||||
|  |         "allowUnusedLabels": false, | ||||||
|  |         "useDefineForClassFields": false, | ||||||
|  |         "useUnknownInCatchVariables": true, | ||||||
|  |         "alwaysStrict": true, | ||||||
|  |         "noImplicitAny": true, | ||||||
|  |         "plugins": [ | ||||||
|  |             { | ||||||
|  |                 "name": "ts-lit-plugin", | ||||||
|  |                 "strict": true, | ||||||
|  |                 "rules": { | ||||||
|  |                     "no-unknown-tag-name": "off", | ||||||
|  |                     "no-missing-import": "off", | ||||||
|  |                     "no-incompatible-type-binding": "off", | ||||||
|  |                     "no-unknown-property": "off", | ||||||
|  |                     "no-unknown-attribute": "off" | ||||||
|  |                 } | ||||||
|  |             }, | ||||||
|  |             { | ||||||
|  |                 "name": "@genesiscommunitysuccess/custom-elements-lsp", | ||||||
|  |                 "designSystemPrefix": "ak-", | ||||||
|  |                 "parser": { | ||||||
|  |                     "timeout": 2000 | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         ] | ||||||
|  |     }, | ||||||
|  |     "exclude": ["src/**/*.test.ts", "./tests"] | ||||||
|  | } | ||||||
| @ -1,6 +0,0 @@ | |||||||
| // @file TSConfig used by the web package during build. |  | ||||||
|  |  | ||||||
| { |  | ||||||
|     "extends": "./tsconfig.json", |  | ||||||
|     "exclude": ["src/**/*.test.ts", "./tests"] |  | ||||||
| } |  | ||||||
| @ -1,19 +1,7 @@ | |||||||
| // @file TSConfig used by the web package during development. |  | ||||||
| { | { | ||||||
|     "extends": "@goauthentik/tsconfig", |     "extends": "./tsconfig.base.json", | ||||||
|     "compilerOptions": { |     "compilerOptions": { | ||||||
|         "allowSyntheticDefaultImports": true, |         "types": ["@wdio/types", "grecaptcha", "node", "@wdio/mocha-framework", "expect-webdriverio"], | ||||||
|         "emitDeclarationOnly": true, |  | ||||||
|         "experimentalDecorators": true, |  | ||||||
|         // See https://lit.dev/docs/components/properties/ |  | ||||||
|         "useDefineForClassFields": false, |  | ||||||
|         "target": "esnext", |  | ||||||
|         "module": "esnext", |  | ||||||
|         "moduleResolution": "bundler", |  | ||||||
|         "baseUrl": ".", |  | ||||||
|         "lib": ["DOM", "DOM.Iterable", "ESNext"], |  | ||||||
|         // TODO: We should enable this when when we're ready to enforce it. |  | ||||||
|         "noUncheckedIndexedAccess": false, |  | ||||||
|         "paths": { |         "paths": { | ||||||
|             "@goauthentik/admin/*": ["./src/admin/*"], |             "@goauthentik/admin/*": ["./src/admin/*"], | ||||||
|             "@goauthentik/common/*": ["./src/common/*"], |             "@goauthentik/common/*": ["./src/common/*"], | ||||||
| @ -25,41 +13,6 @@ | |||||||
|             "@goauthentik/polyfill/*": ["./src/polyfill/*"], |             "@goauthentik/polyfill/*": ["./src/polyfill/*"], | ||||||
|             "@goauthentik/standalone/*": ["./src/standalone/*"], |             "@goauthentik/standalone/*": ["./src/standalone/*"], | ||||||
|             "@goauthentik/user/*": ["./src/user/*"] |             "@goauthentik/user/*": ["./src/user/*"] | ||||||
|         }, |  | ||||||
|         "plugins": [ |  | ||||||
|             { |  | ||||||
|                 "name": "ts-lit-plugin", |  | ||||||
|                 "strict": true, |  | ||||||
|                 "rules": { |  | ||||||
|                     "no-unknown-tag-name": "off", |  | ||||||
|                     "no-missing-import": "off", |  | ||||||
|                     "no-incompatible-type-binding": "off", |  | ||||||
|                     "no-unknown-property": "off", |  | ||||||
|                     "no-unknown-attribute": "off" |  | ||||||
|         } |         } | ||||||
|     }, |     }, | ||||||
|             { |  | ||||||
|                 "name": "@genesiscommunitysuccess/custom-elements-lsp", |  | ||||||
|                 "designSystemPrefix": "ak-", |  | ||||||
|                 "parser": { |  | ||||||
|                     "timeout": 2000 |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         ] |  | ||||||
|     }, |  | ||||||
|     "exclude": [ |  | ||||||
|         // --- |  | ||||||
|         "./out/**/*", |  | ||||||
|         "./dist/**/*", |  | ||||||
|         "src/**/*.test.ts", |  | ||||||
|         "./tests", |  | ||||||
|  |  | ||||||
|         // TODO: Remove after monorepo cleanup. |  | ||||||
|         "src/**/*.comp.ts" |  | ||||||
|     ], |  | ||||||
|     "references": [ |  | ||||||
|         { |  | ||||||
|             "path": "./packages/esbuild-plugin-live-reload" |  | ||||||
|         } |  | ||||||
|     ], |  | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,4 +1,3 @@ | |||||||
| // @file TSConfig used during tests. |  | ||||||
| { | { | ||||||
|     "compilerOptions": { |     "compilerOptions": { | ||||||
|         "baseUrl": ".", |         "baseUrl": ".", | ||||||
|  | |||||||
							
								
								
									
										26
									
								
								web/types/global.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										26
									
								
								web/types/global.d.ts
									
									
									
									
										vendored
									
									
								
							| @ -1,26 +0,0 @@ | |||||||
| /** |  | ||||||
|  * @file Environment variables available via ESBuild. |  | ||||||
|  */ |  | ||||||
|  |  | ||||||
| declare module "process" { |  | ||||||
|     global { |  | ||||||
|         namespace NodeJS { |  | ||||||
|             interface ProcessEnv { |  | ||||||
|                 NODE_ENV: "production" | "development"; |  | ||||||
|                 /** |  | ||||||
|                  * |  | ||||||
|                  * @todo Determine where this is used and if it is needed, |  | ||||||
|                  * give it a better name. |  | ||||||
|                  * @deprecated |  | ||||||
|                  */ |  | ||||||
|                 CWD: string; |  | ||||||
|                 /** |  | ||||||
|                  * @todo Determine where this is used and if it is needed, |  | ||||||
|                  * give it a better name. |  | ||||||
|                  * @deprecated |  | ||||||
|                  */ |  | ||||||
|                 AK_API_BASE_PATH: string; |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @ -336,7 +336,6 @@ export const config: Options.Testrunner = { | |||||||
|         { error: _error, result: _result, duration: _duration, passed: _passed, retries: _retries }, |         { error: _error, result: _result, duration: _duration, passed: _passed, retries: _retries }, | ||||||
|     ) { |     ) { | ||||||
|         if (lemmeSee) { |         if (lemmeSee) { | ||||||
|             // @ts-expect-error TODO |  | ||||||
|             await browser.pause(500); |             await browser.pause(500); | ||||||
|         } |         } | ||||||
|     }, |     }, | ||||||
|  | |||||||
| @ -9146,21 +9146,6 @@ Bindings to groups/users are checked against the user of the event.</source> | |||||||
| </trans-unit> | </trans-unit> | ||||||
| <trans-unit id="sdc02c276ed429008"> | <trans-unit id="sdc02c276ed429008"> | ||||||
|   <source>How to perform authentication during an authorization_code token request flow</source> |   <source>How to perform authentication during an authorization_code token request flow</source> | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s844baf19a6c4a9b4"> |  | ||||||
|   <source>Enable "Remember me on this device"</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="sfa72bca733f40692"> |  | ||||||
|   <source>When enabled, the user can save their username in a cookie, allowing them to skip directly to entering their password.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s1c336c2d6cef77b3"> |  | ||||||
|   <source>Remember me on this device</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s86cf007b861152ca"> |  | ||||||
|   <source>Ensure that the user's new password is different from their previous passwords. The number of past passwords to check is configurable.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s79b3fcd40dd63921"> |  | ||||||
|   <source>Number of previous passwords to check</source> |  | ||||||
| </trans-unit> | </trans-unit> | ||||||
|     </body> |     </body> | ||||||
|   </file> |   </file> | ||||||
|  | |||||||
| @ -7679,21 +7679,6 @@ Bindings to groups/users are checked against the user of the event.</source> | |||||||
| </trans-unit> | </trans-unit> | ||||||
| <trans-unit id="sdc02c276ed429008"> | <trans-unit id="sdc02c276ed429008"> | ||||||
|   <source>How to perform authentication during an authorization_code token request flow</source> |   <source>How to perform authentication during an authorization_code token request flow</source> | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s844baf19a6c4a9b4"> |  | ||||||
|   <source>Enable "Remember me on this device"</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="sfa72bca733f40692"> |  | ||||||
|   <source>When enabled, the user can save their username in a cookie, allowing them to skip directly to entering their password.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s1c336c2d6cef77b3"> |  | ||||||
|   <source>Remember me on this device</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s86cf007b861152ca"> |  | ||||||
|   <source>Ensure that the user's new password is different from their previous passwords. The number of past passwords to check is configurable.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s79b3fcd40dd63921"> |  | ||||||
|   <source>Number of previous passwords to check</source> |  | ||||||
| </trans-unit> | </trans-unit> | ||||||
|     </body> |     </body> | ||||||
|   </file> |   </file> | ||||||
|  | |||||||
| @ -9238,21 +9238,6 @@ Las vinculaciones a grupos o usuarios se comparan con el usuario del evento.</ta | |||||||
| </trans-unit> | </trans-unit> | ||||||
| <trans-unit id="sdc02c276ed429008"> | <trans-unit id="sdc02c276ed429008"> | ||||||
|   <source>How to perform authentication during an authorization_code token request flow</source> |   <source>How to perform authentication during an authorization_code token request flow</source> | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s844baf19a6c4a9b4"> |  | ||||||
|   <source>Enable "Remember me on this device"</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="sfa72bca733f40692"> |  | ||||||
|   <source>When enabled, the user can save their username in a cookie, allowing them to skip directly to entering their password.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s1c336c2d6cef77b3"> |  | ||||||
|   <source>Remember me on this device</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s86cf007b861152ca"> |  | ||||||
|   <source>Ensure that the user's new password is different from their previous passwords. The number of past passwords to check is configurable.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s79b3fcd40dd63921"> |  | ||||||
|   <source>Number of previous passwords to check</source> |  | ||||||
| </trans-unit> | </trans-unit> | ||||||
|     </body> |     </body> | ||||||
|   </file> |   </file> | ||||||
|  | |||||||
| @ -9772,34 +9772,15 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti | |||||||
| </trans-unit> | </trans-unit> | ||||||
| <trans-unit id="s4d5cb134999b50df"> | <trans-unit id="s4d5cb134999b50df"> | ||||||
|   <source>HTTP Basic Auth</source> |   <source>HTTP Basic Auth</source> | ||||||
|   <target>HTTP Basic Auth</target> |  | ||||||
| </trans-unit> | </trans-unit> | ||||||
| <trans-unit id="s6927635d1c339cfc"> | <trans-unit id="s6927635d1c339cfc"> | ||||||
|   <source>Include the client ID and secret as request parameters</source> |   <source>Include the client ID and secret as request parameters</source> | ||||||
|   <target>Inclure le client ID et secret comme paramètres de la requête</target> |  | ||||||
| </trans-unit> | </trans-unit> | ||||||
| <trans-unit id="s4fca384c634e1a92"> | <trans-unit id="s4fca384c634e1a92"> | ||||||
|   <source>Authorization code authentication method</source> |   <source>Authorization code authentication method</source> | ||||||
|   <target>Méthode d'authentification pour authorization_code</target> |  | ||||||
| </trans-unit> | </trans-unit> | ||||||
| <trans-unit id="sdc02c276ed429008"> | <trans-unit id="sdc02c276ed429008"> | ||||||
|   <source>How to perform authentication during an authorization_code token request flow</source> |   <source>How to perform authentication during an authorization_code token request flow</source> | ||||||
|   <target>Comment effectuer l'authentification lors d'une demande de jeton pour le flux authorization_code</target> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s844baf19a6c4a9b4"> |  | ||||||
|   <source>Enable "Remember me on this device"</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="sfa72bca733f40692"> |  | ||||||
|   <source>When enabled, the user can save their username in a cookie, allowing them to skip directly to entering their password.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s1c336c2d6cef77b3"> |  | ||||||
|   <source>Remember me on this device</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s86cf007b861152ca"> |  | ||||||
|   <source>Ensure that the user's new password is different from their previous passwords. The number of past passwords to check is configurable.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s79b3fcd40dd63921"> |  | ||||||
|   <source>Number of previous passwords to check</source> |  | ||||||
| </trans-unit> | </trans-unit> | ||||||
|     </body> |     </body> | ||||||
|   </file> |   </file> | ||||||
|  | |||||||
| @ -9763,21 +9763,6 @@ Bindings to groups/users are checked against the user of the event.</source> | |||||||
| </trans-unit> | </trans-unit> | ||||||
| <trans-unit id="sdc02c276ed429008"> | <trans-unit id="sdc02c276ed429008"> | ||||||
|   <source>How to perform authentication during an authorization_code token request flow</source> |   <source>How to perform authentication during an authorization_code token request flow</source> | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s844baf19a6c4a9b4"> |  | ||||||
|   <source>Enable "Remember me on this device"</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="sfa72bca733f40692"> |  | ||||||
|   <source>When enabled, the user can save their username in a cookie, allowing them to skip directly to entering their password.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s1c336c2d6cef77b3"> |  | ||||||
|   <source>Remember me on this device</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s86cf007b861152ca"> |  | ||||||
|   <source>Ensure that the user's new password is different from their previous passwords. The number of past passwords to check is configurable.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s79b3fcd40dd63921"> |  | ||||||
|   <source>Number of previous passwords to check</source> |  | ||||||
| </trans-unit> | </trans-unit> | ||||||
|     </body> |     </body> | ||||||
|   </file> |   </file> | ||||||
|  | |||||||
| @ -9146,21 +9146,6 @@ Bindings to groups/users are checked against the user of the event.</source> | |||||||
| </trans-unit> | </trans-unit> | ||||||
| <trans-unit id="sdc02c276ed429008"> | <trans-unit id="sdc02c276ed429008"> | ||||||
|   <source>How to perform authentication during an authorization_code token request flow</source> |   <source>How to perform authentication during an authorization_code token request flow</source> | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s844baf19a6c4a9b4"> |  | ||||||
|   <source>Enable "Remember me on this device"</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="sfa72bca733f40692"> |  | ||||||
|   <source>When enabled, the user can save their username in a cookie, allowing them to skip directly to entering their password.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s1c336c2d6cef77b3"> |  | ||||||
|   <source>Remember me on this device</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s86cf007b861152ca"> |  | ||||||
|   <source>Ensure that the user's new password is different from their previous passwords. The number of past passwords to check is configurable.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s79b3fcd40dd63921"> |  | ||||||
|   <source>Number of previous passwords to check</source> |  | ||||||
| </trans-unit> | </trans-unit> | ||||||
|     </body> |     </body> | ||||||
|   </file> |   </file> | ||||||
|  | |||||||
| @ -9048,21 +9048,6 @@ Bindingen naar groepen/gebruikers worden gecontroleerd tegen de gebruiker van de | |||||||
| </trans-unit> | </trans-unit> | ||||||
| <trans-unit id="sdc02c276ed429008"> | <trans-unit id="sdc02c276ed429008"> | ||||||
|   <source>How to perform authentication during an authorization_code token request flow</source> |   <source>How to perform authentication during an authorization_code token request flow</source> | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s844baf19a6c4a9b4"> |  | ||||||
|   <source>Enable "Remember me on this device"</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="sfa72bca733f40692"> |  | ||||||
|   <source>When enabled, the user can save their username in a cookie, allowing them to skip directly to entering their password.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s1c336c2d6cef77b3"> |  | ||||||
|   <source>Remember me on this device</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s86cf007b861152ca"> |  | ||||||
|   <source>Ensure that the user's new password is different from their previous passwords. The number of past passwords to check is configurable.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s79b3fcd40dd63921"> |  | ||||||
|   <source>Number of previous passwords to check</source> |  | ||||||
| </trans-unit> | </trans-unit> | ||||||
|     </body> |     </body> | ||||||
|   </file> |   </file> | ||||||
|  | |||||||
| @ -9473,21 +9473,6 @@ Powiązania z grupami/użytkownikami są sprawdzane względem użytkownika zdarz | |||||||
| </trans-unit> | </trans-unit> | ||||||
| <trans-unit id="sdc02c276ed429008"> | <trans-unit id="sdc02c276ed429008"> | ||||||
|   <source>How to perform authentication during an authorization_code token request flow</source> |   <source>How to perform authentication during an authorization_code token request flow</source> | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s844baf19a6c4a9b4"> |  | ||||||
|   <source>Enable "Remember me on this device"</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="sfa72bca733f40692"> |  | ||||||
|   <source>When enabled, the user can save their username in a cookie, allowing them to skip directly to entering their password.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s1c336c2d6cef77b3"> |  | ||||||
|   <source>Remember me on this device</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s86cf007b861152ca"> |  | ||||||
|   <source>Ensure that the user's new password is different from their previous passwords. The number of past passwords to check is configurable.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s79b3fcd40dd63921"> |  | ||||||
|   <source>Number of previous passwords to check</source> |  | ||||||
| </trans-unit> | </trans-unit> | ||||||
|     </body> |     </body> | ||||||
|   </file> |   </file> | ||||||
|  | |||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user
	