Compare commits
86 Commits
version/20
...
restricted
Author | SHA1 | Date | |
---|---|---|---|
efdecf949d | |||
9ea5f56715 | |||
8228b56b75 | |||
518e10dbdb | |||
79ddad28a8 | |||
beeec85c15 | |||
0561b8d578 | |||
201481bde3 | |||
7d04903d5b | |||
db7d880116 | |||
259cc81723 | |||
c5b099856d | |||
6d912be7f6 | |||
0c54d266d3 | |||
c4784cf383 | |||
44ccbe2fdf | |||
d2615f0d6a | |||
5ab3cf4952 | |||
1926a472cd | |||
d220ca6bab | |||
759ea731bf | |||
e01fd5eb1a | |||
e716e24ec6 | |||
e9c84b8bfb | |||
130adf9d26 | |||
6aab505cd7 | |||
a9c597bc08 | |||
853239dff9 | |||
8f8c3e4944 | |||
dde9960b9c | |||
b1e48a6c1a | |||
b704e9031e | |||
15ef5dc792 | |||
6c4a1850b0 | |||
183d036f3c | |||
b324dc0ce2 | |||
6ad7be65ec | |||
8bf335a2a5 | |||
45709770f4 | |||
6158dd80ca | |||
468d26c587 | |||
c39a97ca58 | |||
8f0810ebb3 | |||
98e0f12d17 | |||
8d37e83df7 | |||
a306bb8384 | |||
c80116475b | |||
2997382df2 | |||
65e48907d3 | |||
1c4848ed8f | |||
64f7fa62dd | |||
16abaa8016 | |||
4cc4a3e4b8 | |||
8abe1f61ea | |||
6712095d7e | |||
5ab308bfd7 | |||
8b93fbcc69 | |||
f641670139 | |||
80af26ef50 | |||
64ce170882 | |||
b6171aa1a4 | |||
087582abbd | |||
6b6d88b81b | |||
55e5d36df5 | |||
fc43e841c9 | |||
895ed6fbdc | |||
f3965261c5 | |||
34ee6dc2b7 | |||
55fe4b0bc0 | |||
8d745609f9 | |||
55edb10da0 | |||
66e4b3af36 | |||
d44fc7790e | |||
291972628a | |||
019221c433 | |||
b99fa9f8f8 | |||
5bde2772c3 | |||
10884a7770 | |||
e858d09d28 | |||
856717395e | |||
b7793200de | |||
bcc0323523 | |||
643c1f5bbf | |||
1fca246839 | |||
b73e68a94c | |||
f9d3c4c9a7 |
@ -1,5 +1,5 @@
|
||||
[bumpversion]
|
||||
current_version = 2024.4.2
|
||||
current_version = 2024.4.1
|
||||
tag = True
|
||||
commit = True
|
||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-(?P<rc_t>[a-zA-Z-]+)(?P<rc_n>[1-9]\\d*))?
|
||||
|
@ -12,7 +12,7 @@ should_build = str(os.environ.get("DOCKER_USERNAME", None) is not None).lower()
|
||||
branch_name = os.environ["GITHUB_REF"]
|
||||
if os.environ.get("GITHUB_HEAD_REF", "") != "":
|
||||
branch_name = os.environ["GITHUB_HEAD_REF"]
|
||||
safe_branch_name = branch_name.replace("refs/heads/", "").replace("/", "-").replace("'", "-")
|
||||
safe_branch_name = branch_name.replace("refs/heads/", "").replace("/", "-")
|
||||
|
||||
image_names = os.getenv("IMAGE_NAME").split(",")
|
||||
image_arch = os.getenv("IMAGE_ARCH") or None
|
||||
@ -54,9 +54,9 @@ image_main_tag = image_tags[0]
|
||||
image_tags_rendered = ",".join(image_tags)
|
||||
|
||||
with open(os.environ["GITHUB_OUTPUT"], "a+", encoding="utf-8") as _output:
|
||||
print("shouldBuild=%s" % should_build, file=_output)
|
||||
print("sha=%s" % sha, file=_output)
|
||||
print("version=%s" % version, file=_output)
|
||||
print("prerelease=%s" % prerelease, file=_output)
|
||||
print("imageTags=%s" % image_tags_rendered, file=_output)
|
||||
print("imageMainTag=%s" % image_main_tag, file=_output)
|
||||
print(f"shouldBuild={should_build}", file=_output)
|
||||
print(f"sha={sha}", file=_output)
|
||||
print(f"version={version}", file=_output)
|
||||
print(f"prerelease={prerelease}", file=_output)
|
||||
print(f"imageTags={image_tags_rendered}", file=_output)
|
||||
print(f"imageMainTag={image_main_tag}", file=_output)
|
||||
|
2
.github/workflows/ci-main.yml
vendored
2
.github/workflows/ci-main.yml
vendored
@ -130,7 +130,7 @@ jobs:
|
||||
- name: Setup authentik env
|
||||
uses: ./.github/actions/setup
|
||||
- name: Create k8s Kind Cluster
|
||||
uses: helm/kind-action@v1.9.0
|
||||
uses: helm/kind-action@v1.10.0
|
||||
- name: run integration
|
||||
run: |
|
||||
poetry run coverage run manage.py test tests/integration
|
||||
|
2
.github/workflows/ci-outpost.yml
vendored
2
.github/workflows/ci-outpost.yml
vendored
@ -29,7 +29,7 @@ jobs:
|
||||
- name: Generate API
|
||||
run: make gen-client-go
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v4
|
||||
uses: golangci/golangci-lint-action@v5
|
||||
with:
|
||||
version: v1.54.2
|
||||
args: --timeout 5000s --verbose
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
from os import environ
|
||||
|
||||
__version__ = "2024.4.2"
|
||||
__version__ = "2024.4.1"
|
||||
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
||||
|
||||
|
||||
|
@ -73,6 +73,11 @@ def auth_user_lookup(raw_header: bytes) -> User | None:
|
||||
if user:
|
||||
CTX_AUTH_VIA.set("secret_key")
|
||||
return user
|
||||
# then try to auth via expression JWT
|
||||
user = token_expression_jwt(auth_credentials)
|
||||
if user:
|
||||
CTX_AUTH_VIA.set("expression_jwt")
|
||||
return user
|
||||
raise AuthenticationFailed("Token invalid/expired")
|
||||
|
||||
|
||||
@ -90,6 +95,13 @@ def token_secret_key(value: str) -> User | None:
|
||||
return outpost.user
|
||||
|
||||
|
||||
def token_expression_jwt(value: str) -> User | None:
|
||||
"""Authenticate API call made by Expressions"""
|
||||
from authentik.lib.expression.evaluator import authenticate_token
|
||||
|
||||
return authenticate_token(value)
|
||||
|
||||
|
||||
class TokenAuthentication(BaseAuthentication):
|
||||
"""Token-based authentication using HTTP Bearer authentication"""
|
||||
|
||||
|
@ -36,7 +36,7 @@ class PropertyMappingEvaluator(BaseEvaluator):
|
||||
_filename = model.name
|
||||
else:
|
||||
_filename = str(model)
|
||||
super().__init__(filename=_filename)
|
||||
super().__init__(None, filename=_filename)
|
||||
req = PolicyRequest(user=User())
|
||||
req.obj = model
|
||||
if user:
|
||||
|
@ -100,6 +100,8 @@ class SourceFlowManager:
|
||||
if self.request.user.is_authenticated:
|
||||
new_connection.user = self.request.user
|
||||
new_connection = self.update_connection(new_connection, **kwargs)
|
||||
|
||||
new_connection.save()
|
||||
return Action.LINK, new_connection
|
||||
|
||||
existing_connections = self.connection_type.objects.filter(
|
||||
@ -146,6 +148,7 @@ class SourceFlowManager:
|
||||
]:
|
||||
new_connection.user = user
|
||||
new_connection = self.update_connection(new_connection, **kwargs)
|
||||
new_connection.save()
|
||||
return Action.LINK, new_connection
|
||||
if self.source.user_matching_mode in [
|
||||
SourceUserMatchingModes.EMAIL_DENY,
|
||||
|
@ -2,9 +2,7 @@
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from django.conf import ImproperlyConfigured
|
||||
from django.contrib.sessions.backends.cache import KEY_PREFIX
|
||||
from django.contrib.sessions.backends.db import SessionStore as DBSessionStore
|
||||
from django.core.cache import cache
|
||||
from django.utils.timezone import now
|
||||
from structlog.stdlib import get_logger
|
||||
@ -17,7 +15,6 @@ from authentik.core.models import (
|
||||
User,
|
||||
)
|
||||
from authentik.events.system_tasks import SystemTask, TaskStatus, prefill_task
|
||||
from authentik.lib.config import CONFIG
|
||||
from authentik.root.celery import CELERY_APP
|
||||
|
||||
LOGGER = get_logger()
|
||||
@ -42,31 +39,16 @@ def clean_expired_models(self: SystemTask):
|
||||
amount = 0
|
||||
|
||||
for session in AuthenticatedSession.objects.all():
|
||||
match CONFIG.get("session_storage", "cache"):
|
||||
case "cache":
|
||||
cache_key = f"{KEY_PREFIX}{session.session_key}"
|
||||
value = None
|
||||
try:
|
||||
value = cache.get(cache_key)
|
||||
cache_key = f"{KEY_PREFIX}{session.session_key}"
|
||||
value = None
|
||||
try:
|
||||
value = cache.get(cache_key)
|
||||
|
||||
except Exception as exc:
|
||||
LOGGER.debug("Failed to get session from cache", exc=exc)
|
||||
if not value:
|
||||
session.delete()
|
||||
amount += 1
|
||||
case "db":
|
||||
if not (
|
||||
DBSessionStore.get_model_class()
|
||||
.objects.filter(session_key=session.session_key, expire_date__gt=now())
|
||||
.exists()
|
||||
):
|
||||
session.delete()
|
||||
amount += 1
|
||||
case _:
|
||||
# Should never happen, as we check for other values in authentik/root/settings.py
|
||||
raise ImproperlyConfigured(
|
||||
"Invalid session_storage setting, allowed values are db and cache"
|
||||
)
|
||||
except Exception as exc:
|
||||
LOGGER.debug("Failed to get session from cache", exc=exc)
|
||||
if not value:
|
||||
session.delete()
|
||||
amount += 1
|
||||
LOGGER.debug("Expired sessions", model=AuthenticatedSession, amount=amount)
|
||||
|
||||
messages.append(f"Expired {amount} {AuthenticatedSession._meta.verbose_name_plural}")
|
||||
|
@ -66,14 +66,11 @@ class TestPropertyMappings(TestCase):
|
||||
expression="return request.http_request.path",
|
||||
)
|
||||
http_request = self.factory.get("/")
|
||||
tmpl = (
|
||||
"""
|
||||
res = ak_call_policy('%s')
|
||||
tmpl = f"""
|
||||
res = ak_call_policy('{expr.name}')
|
||||
result = [request.http_request.path, res.raw_result]
|
||||
return result
|
||||
"""
|
||||
% expr.name
|
||||
)
|
||||
evaluator = PropertyMapping(expression=tmpl, name=generate_id())
|
||||
res = evaluator.evaluate(self.user, http_request)
|
||||
self.assertEqual(res, ["/", "/"])
|
||||
|
@ -48,21 +48,15 @@ class TestSourceFlowManager(TestCase):
|
||||
|
||||
def test_authenticated_link(self):
|
||||
"""Test authenticated user linking"""
|
||||
UserOAuthSourceConnection.objects.create(
|
||||
user=get_anonymous_user(), source=self.source, identifier=self.identifier
|
||||
)
|
||||
user = User.objects.create(username="foo", email="foo@bar.baz")
|
||||
flow_manager = OAuthSourceFlowManager(
|
||||
self.source, get_request("/", user=user), self.identifier, {}
|
||||
)
|
||||
action, connection = flow_manager.get_action()
|
||||
action, _ = flow_manager.get_action()
|
||||
self.assertEqual(action, Action.LINK)
|
||||
self.assertIsNone(connection.pk)
|
||||
flow_manager.get_flow()
|
||||
|
||||
def test_unauthenticated_link(self):
|
||||
"""Test un-authenticated user linking"""
|
||||
flow_manager = OAuthSourceFlowManager(self.source, get_request("/"), self.identifier, {})
|
||||
action, connection = flow_manager.get_action()
|
||||
self.assertEqual(action, Action.LINK)
|
||||
self.assertIsNone(connection.pk)
|
||||
flow_manager.get_flow()
|
||||
|
||||
def test_unauthenticated_enroll_email(self):
|
||||
|
@ -4,7 +4,7 @@ from django.utils.text import slugify
|
||||
|
||||
from authentik.brands.models import Brand
|
||||
from authentik.core.models import Group, User
|
||||
from authentik.crypto.builder import CertificateBuilder, PrivateKeyAlg
|
||||
from authentik.crypto.builder import CertificateBuilder
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.flows.models import Flow, FlowDesignation
|
||||
from authentik.lib.generators import generate_id
|
||||
@ -50,10 +50,12 @@ def create_test_brand(**kwargs) -> Brand:
|
||||
return Brand.objects.create(domain=uid, default=True, **kwargs)
|
||||
|
||||
|
||||
def create_test_cert(alg=PrivateKeyAlg.RSA) -> CertificateKeyPair:
|
||||
def create_test_cert(use_ec_private_key=False) -> CertificateKeyPair:
|
||||
"""Generate a certificate for testing"""
|
||||
builder = CertificateBuilder(f"{generate_id()}.self-signed.goauthentik.io")
|
||||
builder.alg = alg
|
||||
builder = CertificateBuilder(
|
||||
name=f"{generate_id()}.self-signed.goauthentik.io",
|
||||
use_ec_private_key=use_ec_private_key,
|
||||
)
|
||||
builder.build(
|
||||
subject_alt_names=[f"{generate_id()}.self-signed.goauthentik.io"],
|
||||
validity_days=360,
|
||||
|
@ -14,13 +14,7 @@ from drf_spectacular.types import OpenApiTypes
|
||||
from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.fields import (
|
||||
CharField,
|
||||
ChoiceField,
|
||||
DateTimeField,
|
||||
IntegerField,
|
||||
SerializerMethodField,
|
||||
)
|
||||
from rest_framework.fields import CharField, DateTimeField, IntegerField, SerializerMethodField
|
||||
from rest_framework.filters import OrderingFilter, SearchFilter
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
@ -32,7 +26,7 @@ from authentik.api.authorization import SecretKeyFilter
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.core.api.utils import PassiveSerializer
|
||||
from authentik.crypto.apps import MANAGED_KEY
|
||||
from authentik.crypto.builder import CertificateBuilder, PrivateKeyAlg
|
||||
from authentik.crypto.builder import CertificateBuilder
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.rbac.decorators import permission_required
|
||||
@ -184,7 +178,6 @@ class CertificateGenerationSerializer(PassiveSerializer):
|
||||
common_name = CharField()
|
||||
subject_alt_name = CharField(required=False, allow_blank=True, label=_("Subject-alt name"))
|
||||
validity_days = IntegerField(initial=365)
|
||||
alg = ChoiceField(default=PrivateKeyAlg.RSA, choices=PrivateKeyAlg.choices)
|
||||
|
||||
|
||||
class CertificateKeyPairFilter(FilterSet):
|
||||
@ -247,7 +240,6 @@ class CertificateKeyPairViewSet(UsedByMixin, ModelViewSet):
|
||||
raw_san = data.validated_data.get("subject_alt_name", "")
|
||||
sans = raw_san.split(",") if raw_san != "" else []
|
||||
builder = CertificateBuilder(data.validated_data["common_name"])
|
||||
builder.alg = data.validated_data["alg"]
|
||||
builder.build(
|
||||
subject_alt_names=sans,
|
||||
validity_days=int(data.validated_data["validity_days"]),
|
||||
|
@ -9,28 +9,20 @@ from cryptography.hazmat.primitives import hashes, serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import ec, rsa
|
||||
from cryptography.hazmat.primitives.asymmetric.types import PrivateKeyTypes
|
||||
from cryptography.x509.oid import NameOID
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from authentik import __version__
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
|
||||
|
||||
class PrivateKeyAlg(models.TextChoices):
|
||||
"""Algorithm to create private key with"""
|
||||
|
||||
RSA = "rsa", _("rsa")
|
||||
ECDSA = "ecdsa", _("ecdsa")
|
||||
|
||||
|
||||
class CertificateBuilder:
|
||||
"""Build self-signed certificates"""
|
||||
|
||||
common_name: str
|
||||
alg: PrivateKeyAlg
|
||||
|
||||
def __init__(self, name: str):
|
||||
self.alg = PrivateKeyAlg.RSA
|
||||
_use_ec_private_key: bool
|
||||
|
||||
def __init__(self, name: str, use_ec_private_key=False):
|
||||
self._use_ec_private_key = use_ec_private_key
|
||||
self.__public_key = None
|
||||
self.__private_key = None
|
||||
self.__builder = None
|
||||
@ -50,13 +42,11 @@ class CertificateBuilder:
|
||||
|
||||
def generate_private_key(self) -> PrivateKeyTypes:
|
||||
"""Generate private key"""
|
||||
if self.alg == PrivateKeyAlg.ECDSA:
|
||||
if self._use_ec_private_key:
|
||||
return ec.generate_private_key(curve=ec.SECP256R1())
|
||||
if self.alg == PrivateKeyAlg.RSA:
|
||||
return rsa.generate_private_key(
|
||||
public_exponent=65537, key_size=4096, backend=default_backend()
|
||||
)
|
||||
raise ValueError(f"Invalid alg: {self.alg}")
|
||||
return rsa.generate_private_key(
|
||||
public_exponent=65537, key_size=4096, backend=default_backend()
|
||||
)
|
||||
|
||||
def build(
|
||||
self,
|
||||
|
@ -2,12 +2,11 @@
|
||||
|
||||
from copy import deepcopy
|
||||
from functools import partial
|
||||
from typing import Any
|
||||
|
||||
from django.apps.registry import apps
|
||||
from django.core.files import File
|
||||
from django.db import connection
|
||||
from django.db.models import ManyToManyRel, Model
|
||||
from django.db.models import Model
|
||||
from django.db.models.expressions import BaseExpression, Combinable
|
||||
from django.db.models.signals import post_init
|
||||
from django.http import HttpRequest
|
||||
@ -45,7 +44,7 @@ class EnterpriseAuditMiddleware(AuditMiddleware):
|
||||
post_init.disconnect(dispatch_uid=request.request_id)
|
||||
|
||||
def serialize_simple(self, model: Model) -> dict:
|
||||
"""Serialize a model in a very simple way. No ForeignKeys or other relationships are
|
||||
"""Serialize a model in a very simple way. No ForeginKeys or other relationships are
|
||||
resolved"""
|
||||
data = {}
|
||||
deferred_fields = model.get_deferred_fields()
|
||||
@ -71,9 +70,6 @@ class EnterpriseAuditMiddleware(AuditMiddleware):
|
||||
for key, value in before.items():
|
||||
if after.get(key) != value:
|
||||
diff[key] = {"previous_value": value, "new_value": after.get(key)}
|
||||
for key, value in after.items():
|
||||
if key not in before and key not in diff and before.get(key) != value:
|
||||
diff[key] = {"previous_value": before.get(key), "new_value": value}
|
||||
return sanitize_item(diff)
|
||||
|
||||
def post_init_handler(self, request: HttpRequest, sender, instance: Model, **_):
|
||||
@ -102,37 +98,8 @@ class EnterpriseAuditMiddleware(AuditMiddleware):
|
||||
thread_kwargs = {}
|
||||
if hasattr(instance, "_previous_state") or created:
|
||||
prev_state = getattr(instance, "_previous_state", {})
|
||||
if created:
|
||||
prev_state = {}
|
||||
# Get current state
|
||||
new_state = self.serialize_simple(instance)
|
||||
diff = self.diff(prev_state, new_state)
|
||||
thread_kwargs["diff"] = diff
|
||||
return super().post_save_handler(request, sender, instance, created, thread_kwargs, **_)
|
||||
|
||||
def m2m_changed_handler( # noqa: PLR0913
|
||||
self,
|
||||
request: HttpRequest,
|
||||
sender,
|
||||
instance: Model,
|
||||
action: str,
|
||||
pk_set: set[Any],
|
||||
thread_kwargs: dict | None = None,
|
||||
**_,
|
||||
):
|
||||
thread_kwargs = {}
|
||||
m2m_field = None
|
||||
# For the audit log we don't care about `pre_` or `post_` so we trim that part off
|
||||
_, _, action_direction = action.partition("_")
|
||||
# resolve the "through" model to an actual field
|
||||
for field in instance._meta.get_fields():
|
||||
if not isinstance(field, ManyToManyRel):
|
||||
continue
|
||||
if field.through == sender:
|
||||
m2m_field = field
|
||||
if m2m_field:
|
||||
# If we're clearing we just set the "flag" to True
|
||||
if action_direction == "clear":
|
||||
pk_set = True
|
||||
thread_kwargs["diff"] = {m2m_field.related_name: {action_direction: pk_set}}
|
||||
return super().m2m_changed_handler(request, sender, instance, action, thread_kwargs)
|
||||
|
@ -1,22 +1,9 @@
|
||||
from unittest.mock import PropertyMock, patch
|
||||
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
from django.urls import reverse
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from authentik.core.models import Group, User
|
||||
from authentik.core.tests.utils import create_test_admin_user
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.events.utils import sanitize_item
|
||||
from authentik.lib.generators import generate_id
|
||||
from django.test import TestCase
|
||||
|
||||
|
||||
class TestEnterpriseAudit(APITestCase):
|
||||
"""Test audit middleware"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.user = create_test_admin_user()
|
||||
class TestEnterpriseAudit(TestCase):
|
||||
|
||||
def test_import(self):
|
||||
"""Ensure middleware is imported when app.ready is called"""
|
||||
@ -29,182 +16,3 @@ class TestEnterpriseAudit(APITestCase):
|
||||
self.assertIn(
|
||||
"authentik.enterprise.audit.middleware.EnterpriseAuditMiddleware", settings.MIDDLEWARE
|
||||
)
|
||||
|
||||
@patch(
|
||||
"authentik.enterprise.audit.middleware.EnterpriseAuditMiddleware.enabled",
|
||||
PropertyMock(return_value=True),
|
||||
)
|
||||
def test_create(self):
|
||||
"""Test create audit log"""
|
||||
self.client.force_login(self.user)
|
||||
username = generate_id()
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:user-list"),
|
||||
data={"name": generate_id(), "username": username, "groups": [], "path": "foo"},
|
||||
)
|
||||
user = User.objects.get(username=username)
|
||||
self.assertEqual(response.status_code, 201)
|
||||
events = Event.objects.filter(
|
||||
action=EventAction.MODEL_CREATED,
|
||||
context__model__model_name="user",
|
||||
context__model__app="authentik_core",
|
||||
context__model__pk=user.pk,
|
||||
)
|
||||
event = events.first()
|
||||
self.assertIsNotNone(event)
|
||||
self.assertIsNotNone(event.context["diff"])
|
||||
diff = event.context["diff"]
|
||||
self.assertEqual(
|
||||
diff,
|
||||
{
|
||||
"name": {
|
||||
"new_value": user.name,
|
||||
"previous_value": None,
|
||||
},
|
||||
"path": {"new_value": "foo", "previous_value": None},
|
||||
"type": {"new_value": "internal", "previous_value": None},
|
||||
"uuid": {
|
||||
"new_value": user.uuid.hex,
|
||||
"previous_value": None,
|
||||
},
|
||||
"email": {"new_value": "", "previous_value": None},
|
||||
"username": {
|
||||
"new_value": user.username,
|
||||
"previous_value": None,
|
||||
},
|
||||
"is_active": {"new_value": True, "previous_value": None},
|
||||
"attributes": {"new_value": {}, "previous_value": None},
|
||||
"date_joined": {
|
||||
"new_value": sanitize_item(user.date_joined),
|
||||
"previous_value": None,
|
||||
},
|
||||
"first_name": {"new_value": "", "previous_value": None},
|
||||
"id": {"new_value": user.pk, "previous_value": None},
|
||||
"last_name": {"new_value": "", "previous_value": None},
|
||||
"password": {"new_value": "********************", "previous_value": None},
|
||||
"password_change_date": {
|
||||
"new_value": sanitize_item(user.password_change_date),
|
||||
"previous_value": None,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
@patch(
|
||||
"authentik.enterprise.audit.middleware.EnterpriseAuditMiddleware.enabled",
|
||||
PropertyMock(return_value=True),
|
||||
)
|
||||
def test_update(self):
|
||||
"""Test update audit log"""
|
||||
self.client.force_login(self.user)
|
||||
user = create_test_admin_user()
|
||||
current_name = user.name
|
||||
new_name = generate_id()
|
||||
response = self.client.patch(
|
||||
reverse("authentik_api:user-detail", kwargs={"pk": user.id}),
|
||||
data={"name": new_name},
|
||||
)
|
||||
user.refresh_from_db()
|
||||
self.assertEqual(response.status_code, 200)
|
||||
events = Event.objects.filter(
|
||||
action=EventAction.MODEL_UPDATED,
|
||||
context__model__model_name="user",
|
||||
context__model__app="authentik_core",
|
||||
context__model__pk=user.pk,
|
||||
)
|
||||
event = events.first()
|
||||
self.assertIsNotNone(event)
|
||||
self.assertIsNotNone(event.context["diff"])
|
||||
diff = event.context["diff"]
|
||||
self.assertEqual(
|
||||
diff,
|
||||
{
|
||||
"name": {
|
||||
"new_value": new_name,
|
||||
"previous_value": current_name,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
@patch(
|
||||
"authentik.enterprise.audit.middleware.EnterpriseAuditMiddleware.enabled",
|
||||
PropertyMock(return_value=True),
|
||||
)
|
||||
def test_delete(self):
|
||||
"""Test delete audit log"""
|
||||
self.client.force_login(self.user)
|
||||
user = create_test_admin_user()
|
||||
response = self.client.delete(
|
||||
reverse("authentik_api:user-detail", kwargs={"pk": user.id}),
|
||||
)
|
||||
self.assertEqual(response.status_code, 204)
|
||||
events = Event.objects.filter(
|
||||
action=EventAction.MODEL_DELETED,
|
||||
context__model__model_name="user",
|
||||
context__model__app="authentik_core",
|
||||
context__model__pk=user.pk,
|
||||
)
|
||||
event = events.first()
|
||||
self.assertIsNotNone(event)
|
||||
self.assertNotIn("diff", event.context)
|
||||
|
||||
@patch(
|
||||
"authentik.enterprise.audit.middleware.EnterpriseAuditMiddleware.enabled",
|
||||
PropertyMock(return_value=True),
|
||||
)
|
||||
def test_m2m_add(self):
|
||||
"""Test m2m add audit log"""
|
||||
self.client.force_login(self.user)
|
||||
user = create_test_admin_user()
|
||||
group = Group.objects.create(name=generate_id())
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:group-add-user", kwargs={"pk": group.group_uuid}),
|
||||
data={
|
||||
"pk": user.pk,
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, 204)
|
||||
events = Event.objects.filter(
|
||||
action=EventAction.MODEL_UPDATED,
|
||||
context__model__model_name="group",
|
||||
context__model__app="authentik_core",
|
||||
context__model__pk=group.pk.hex,
|
||||
)
|
||||
event = events.first()
|
||||
self.assertIsNotNone(event)
|
||||
self.assertIsNotNone(event.context["diff"])
|
||||
diff = event.context["diff"]
|
||||
self.assertEqual(
|
||||
diff,
|
||||
{"users": {"add": [user.pk]}},
|
||||
)
|
||||
|
||||
@patch(
|
||||
"authentik.enterprise.audit.middleware.EnterpriseAuditMiddleware.enabled",
|
||||
PropertyMock(return_value=True),
|
||||
)
|
||||
def test_m2m_remove(self):
|
||||
"""Test m2m remove audit log"""
|
||||
self.client.force_login(self.user)
|
||||
user = create_test_admin_user()
|
||||
group = Group.objects.create(name=generate_id())
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:group-remove-user", kwargs={"pk": group.group_uuid}),
|
||||
data={
|
||||
"pk": user.pk,
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, 204)
|
||||
events = Event.objects.filter(
|
||||
action=EventAction.MODEL_UPDATED,
|
||||
context__model__model_name="group",
|
||||
context__model__app="authentik_core",
|
||||
context__model__pk=group.pk.hex,
|
||||
)
|
||||
event = events.first()
|
||||
self.assertIsNotNone(event)
|
||||
self.assertIsNotNone(event.context["diff"])
|
||||
diff = event.context["diff"]
|
||||
self.assertEqual(
|
||||
diff,
|
||||
{"users": {"remove": [user.pk]}},
|
||||
)
|
||||
|
@ -214,15 +214,7 @@ class AuditMiddleware:
|
||||
model=model_to_dict(instance),
|
||||
).run()
|
||||
|
||||
def m2m_changed_handler(
|
||||
self,
|
||||
request: HttpRequest,
|
||||
sender,
|
||||
instance: Model,
|
||||
action: str,
|
||||
thread_kwargs: dict | None = None,
|
||||
**_,
|
||||
):
|
||||
def m2m_changed_handler(self, request: HttpRequest, sender, instance: Model, action: str, **_):
|
||||
"""Signal handler for all object's m2m_changed"""
|
||||
if action not in ["pre_add", "pre_remove", "post_clear"]:
|
||||
return
|
||||
@ -237,5 +229,4 @@ class AuditMiddleware:
|
||||
request,
|
||||
user=user,
|
||||
model=model_to_dict(instance),
|
||||
**thread_kwargs,
|
||||
).run()
|
||||
|
@ -119,7 +119,7 @@ class SystemTask(TenantTask):
|
||||
"task_call_kwargs": sanitize_item(kwargs),
|
||||
"status": self._status,
|
||||
"messages": sanitize_item(self._messages),
|
||||
"expires": now() + timedelta(hours=self.result_timeout_hours + 3),
|
||||
"expires": now() + timedelta(hours=self.result_timeout_hours),
|
||||
"expiring": True,
|
||||
},
|
||||
)
|
||||
|
@ -53,7 +53,6 @@ cache:
|
||||
|
||||
# result_backend:
|
||||
# url: ""
|
||||
# transport_options: ""
|
||||
|
||||
debug: false
|
||||
remote_debug: false
|
||||
@ -96,6 +95,9 @@ outposts:
|
||||
discover: true
|
||||
disable_embedded_outpost: false
|
||||
|
||||
expressions:
|
||||
global_runtime: python # or python_restricted
|
||||
|
||||
ldap:
|
||||
task_timeout_hours: 2
|
||||
page_size: 50
|
||||
|
@ -3,27 +3,117 @@
|
||||
import re
|
||||
import socket
|
||||
from collections.abc import Iterable
|
||||
from datetime import timedelta
|
||||
from functools import lru_cache
|
||||
from ipaddress import ip_address, ip_network
|
||||
from pathlib import Path
|
||||
from tempfile import gettempdir
|
||||
from textwrap import indent
|
||||
from typing import Any
|
||||
|
||||
from authentik_client.api.admin_api import AdminApi
|
||||
from authentik_client.api.authenticators_api import AuthenticatorsApi
|
||||
from authentik_client.api.core_api import CoreApi
|
||||
from authentik_client.api.crypto_api import CryptoApi
|
||||
from authentik_client.api.enterprise_api import EnterpriseApi
|
||||
from authentik_client.api.events_api import EventsApi
|
||||
from authentik_client.api.flows_api import FlowsApi
|
||||
from authentik_client.api.managed_api import ManagedApi
|
||||
from authentik_client.api.oauth2_api import Oauth2Api
|
||||
from authentik_client.api.outposts_api import OutpostsApi
|
||||
from authentik_client.api.policies_api import PoliciesApi
|
||||
from authentik_client.api.propertymappings_api import PropertymappingsApi
|
||||
from authentik_client.api.providers_api import ProvidersApi
|
||||
from authentik_client.api.rac_api import RacApi
|
||||
from authentik_client.api.rbac_api import RbacApi
|
||||
from authentik_client.api.root_api import RootApi
|
||||
from authentik_client.api.schema_api import SchemaApi
|
||||
from authentik_client.api.sources_api import SourcesApi
|
||||
from authentik_client.api.stages_api import StagesApi
|
||||
from authentik_client.api.tenants_api import TenantsApi
|
||||
from authentik_client.api_client import ApiClient
|
||||
from authentik_client.configuration import Configuration
|
||||
from cachetools import TLRUCache, cached
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import FieldError
|
||||
from django.utils.timezone import now
|
||||
from guardian.shortcuts import get_anonymous_user
|
||||
from jwt import PyJWTError, decode, encode
|
||||
from rest_framework.serializers import ValidationError
|
||||
from RestrictedPython import compile_restricted, limited_builtins, safe_builtins, utility_builtins
|
||||
from sentry_sdk.hub import Hub
|
||||
from sentry_sdk.tracing import Span
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.core.models import User
|
||||
from authentik.events.models import Event
|
||||
from authentik.lib.utils.http import get_http_session
|
||||
from authentik.lib.config import CONFIG
|
||||
from authentik.lib.generators import generate_key
|
||||
from authentik.lib.utils.errors import exception_to_string
|
||||
from authentik.lib.utils.http import authentik_user_agent, get_http_session
|
||||
from authentik.lib.utils.reflection import get_apps
|
||||
from authentik.policies.models import Policy, PolicyBinding
|
||||
from authentik.policies.process import PolicyProcess
|
||||
from authentik.policies.types import PolicyRequest, PolicyResult
|
||||
from authentik.stages.authenticator import devices_for_user
|
||||
|
||||
LOGGER = get_logger()
|
||||
_tmp = Path(gettempdir())
|
||||
token_path = _tmp / "authentik-evaluator-token"
|
||||
|
||||
API_CLIENTS = {
|
||||
"AdminApi": AdminApi,
|
||||
"AuthenticatorsApi": AuthenticatorsApi,
|
||||
"CoreApi": CoreApi,
|
||||
"CryptoApi": CryptoApi,
|
||||
"EnterpriseApi": EnterpriseApi,
|
||||
"EventsApi": EventsApi,
|
||||
"FlowsApi": FlowsApi,
|
||||
"ManagedApi": ManagedApi,
|
||||
"Oauth2Api": Oauth2Api,
|
||||
"OutpostsApi": OutpostsApi,
|
||||
"PoliciesApi": PoliciesApi,
|
||||
"PropertymappingsApi": PropertymappingsApi,
|
||||
"ProvidersApi": ProvidersApi,
|
||||
"RacApi": RacApi,
|
||||
"RbacApi": RbacApi,
|
||||
"RootApi": RootApi,
|
||||
"SchemaApi": SchemaApi,
|
||||
"SourcesApi": SourcesApi,
|
||||
"StagesApi": StagesApi,
|
||||
"TenantsApi": TenantsApi,
|
||||
}
|
||||
|
||||
JWT_AUD = "goauthentik.io/api/expression"
|
||||
|
||||
_SAFE_MODULES = frozenset(("authentik_client",))
|
||||
|
||||
|
||||
def _safe_import(name, *args, **kwargs):
|
||||
if name not in _SAFE_MODULES:
|
||||
raise Exception(f"Don't you even think about {name!r}")
|
||||
return __import__(name, *args, **kwargs)
|
||||
|
||||
|
||||
@lru_cache
|
||||
def get_api_token_secret():
|
||||
if token_path.exists():
|
||||
with open(token_path) as _token_file:
|
||||
return _token_file.read()
|
||||
key = generate_key()
|
||||
with open(_tmp / "authentik-evaluator-token", "w") as _token_file:
|
||||
_token_file.write(key)
|
||||
return key
|
||||
|
||||
|
||||
def authenticate_token(raw_value: str):
|
||||
"""Authenticate API call from evaluator token"""
|
||||
try:
|
||||
jwt = decode(raw_value, get_api_token_secret(), ["HS256"], audience=JWT_AUD)
|
||||
return User.objects.filter(pk=jwt["sub"]).first()
|
||||
except PyJWTError as exc:
|
||||
LOGGER.debug("failed to auth", exc=exc)
|
||||
return None
|
||||
|
||||
|
||||
class BaseEvaluator:
|
||||
@ -37,8 +127,14 @@ class BaseEvaluator:
|
||||
# Filename used for exec
|
||||
_filename: str
|
||||
|
||||
def __init__(self, filename: str | None = None):
|
||||
_user: User
|
||||
|
||||
# Timeout in seconds, used for the expiration of the API key
|
||||
timeout = 30
|
||||
|
||||
def __init__(self, user: User, filename: str | None = None):
|
||||
self._filename = filename if filename else "BaseEvaluator"
|
||||
self._user = user
|
||||
# update website/docs/expressions/_objects.md
|
||||
# update website/docs/expressions/_functions.md
|
||||
self._globals = {
|
||||
@ -57,8 +153,44 @@ class BaseEvaluator:
|
||||
"resolve_dns": BaseEvaluator.expr_resolve_dns,
|
||||
"reverse_dns": BaseEvaluator.expr_reverse_dns,
|
||||
}
|
||||
for app in get_apps():
|
||||
# Load models from each app
|
||||
for model in app.get_models():
|
||||
self._globals[model.__name__] = model
|
||||
self._globals.update(API_CLIENTS)
|
||||
self._context = {}
|
||||
|
||||
def get_token(self) -> str:
|
||||
"""Generate API token to be used by the API Client"""
|
||||
_now = now()
|
||||
if not self._user:
|
||||
self._user = get_anonymous_user()
|
||||
return encode(
|
||||
{
|
||||
"aud": JWT_AUD,
|
||||
"iss": f"goauthentik.io/expression/{self._filename}",
|
||||
"sub": str(self._user.pk),
|
||||
"iat": int(_now.timestamp()),
|
||||
"exp": int((_now + timedelta(seconds=self.timeout)).timestamp()),
|
||||
},
|
||||
get_api_token_secret(),
|
||||
)
|
||||
|
||||
def get_api_client(self):
|
||||
token = self.get_token()
|
||||
config = Configuration(
|
||||
f"unix://{str(_tmp.joinpath('authentik-core.sock'))}/api/v3",
|
||||
api_key={
|
||||
"authentik": token,
|
||||
},
|
||||
api_key_prefix={"authentik": "Bearer"},
|
||||
)
|
||||
if settings.DEBUG:
|
||||
config.host = "http://localhost:8000/api/v3"
|
||||
client = ApiClient(config)
|
||||
client.user_agent = authentik_user_agent()
|
||||
return client
|
||||
|
||||
@cached(cache=TLRUCache(maxsize=32, ttu=lambda key, value, now: now + 180))
|
||||
@staticmethod
|
||||
def expr_resolve_dns(host: str, ip_version: int | None = None) -> list[str]:
|
||||
@ -185,7 +317,16 @@ class BaseEvaluator:
|
||||
def compile(self, expression: str) -> Any:
|
||||
"""Parse expression. Raises SyntaxError or ValueError if the syntax is incorrect."""
|
||||
param_keys = self._context.keys()
|
||||
return compile(self.wrap_expression(expression, param_keys), self._filename, "exec")
|
||||
compiler = (
|
||||
compile_restricted
|
||||
if CONFIG.get("expressions.global_runtime") == "python_restricted"
|
||||
else compile
|
||||
)
|
||||
return compiler(
|
||||
self.wrap_expression(expression, param_keys),
|
||||
self._filename,
|
||||
"exec",
|
||||
)
|
||||
|
||||
def evaluate(self, expression_source: str) -> Any:
|
||||
"""Parse and evaluate expression. If the syntax is incorrect, a SyntaxError is raised.
|
||||
@ -201,7 +342,17 @@ class BaseEvaluator:
|
||||
self.handle_error(exc, expression_source)
|
||||
raise exc
|
||||
try:
|
||||
if CONFIG.get("expressions.global_runtime") == "python_restricted":
|
||||
self._globals["__builtins__"] = {
|
||||
**safe_builtins,
|
||||
**limited_builtins,
|
||||
**utility_builtins,
|
||||
"__import__": _safe_import,
|
||||
}
|
||||
_locals = self._context
|
||||
# We need to create the API Client later so that the token is valid
|
||||
# from when the execution starts
|
||||
self._globals["api"] = self.get_api_client()
|
||||
# Yes this is an exec, yes it is potentially bad. Since we limit what variables are
|
||||
# available here, and these policies can only be edited by admins, this is a risk
|
||||
# we're willing to take.
|
||||
@ -209,6 +360,7 @@ class BaseEvaluator:
|
||||
exec(ast_obj, self._globals, _locals) # nosec # noqa
|
||||
result = _locals["result"]
|
||||
except Exception as exc:
|
||||
print(exception_to_string(exc))
|
||||
# So, this is a bit questionable. Essentially, we are edit the stacktrace
|
||||
# so the user only sees information relevant to them
|
||||
# and none of our surrounding error handling
|
||||
|
@ -1,6 +1,7 @@
|
||||
"""Test Evaluator base functions"""
|
||||
|
||||
from django.test import TestCase
|
||||
from guardian.shortcuts import get_anonymous_user
|
||||
|
||||
from authentik.core.tests.utils import create_test_admin_user
|
||||
from authentik.events.models import Event
|
||||
@ -33,7 +34,7 @@ class TestEvaluator(TestCase):
|
||||
|
||||
def test_expr_event_create(self):
|
||||
"""Test expr_event_create"""
|
||||
evaluator = BaseEvaluator(generate_id())
|
||||
evaluator = BaseEvaluator(get_anonymous_user(), generate_id())
|
||||
evaluator._context = {
|
||||
"foo": "bar",
|
||||
}
|
||||
|
@ -14,12 +14,13 @@ class ExpressionPolicySerializer(PolicySerializer):
|
||||
def validate_expression(self, expr: str) -> str:
|
||||
"""validate the syntax of the expression"""
|
||||
name = "temp-policy" if not self.instance else self.instance.name
|
||||
PolicyEvaluator(name).validate(expr)
|
||||
request = self.context.get("request")
|
||||
PolicyEvaluator(request.user if request else None, name).validate(expr)
|
||||
return expr
|
||||
|
||||
class Meta:
|
||||
model = ExpressionPolicy
|
||||
fields = PolicySerializer.Meta.fields + ["expression"]
|
||||
fields = PolicySerializer.Meta.fields + ["expression", "execution_user"]
|
||||
|
||||
|
||||
class ExpressionPolicyViewSet(UsedByMixin, ModelViewSet):
|
||||
|
@ -1,11 +1,12 @@
|
||||
"""Authentik policy_expression app config"""
|
||||
|
||||
from django.apps import AppConfig
|
||||
from authentik.blueprints.apps import ManagedAppConfig
|
||||
|
||||
|
||||
class AuthentikPolicyExpressionConfig(AppConfig):
|
||||
class AuthentikPolicyExpressionConfig(ManagedAppConfig):
|
||||
"""Authentik policy_expression app config"""
|
||||
|
||||
name = "authentik.policies.expression"
|
||||
label = "authentik_policies_expression"
|
||||
verbose_name = "authentik Policies.Expression"
|
||||
default = True
|
||||
|
@ -6,6 +6,7 @@ from typing import TYPE_CHECKING, Optional
|
||||
from django.http import HttpRequest
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.core.models import User
|
||||
from authentik.flows.planner import PLAN_CONTEXT_SSO
|
||||
from authentik.lib.expression.evaluator import BaseEvaluator
|
||||
from authentik.policies.exceptions import PolicyException
|
||||
@ -24,8 +25,8 @@ class PolicyEvaluator(BaseEvaluator):
|
||||
|
||||
policy: Optional["ExpressionPolicy"] = None
|
||||
|
||||
def __init__(self, policy_name: str | None = None):
|
||||
super().__init__(policy_name or "PolicyEvaluator")
|
||||
def __init__(self, user: User, policy_name: str | None = None):
|
||||
super().__init__(user, policy_name or "PolicyEvaluator")
|
||||
self._messages = []
|
||||
# update website/docs/expressions/_objects.md
|
||||
# update website/docs/expressions/_functions.md
|
||||
@ -44,6 +45,8 @@ class PolicyEvaluator(BaseEvaluator):
|
||||
if request.http_request:
|
||||
self.set_http_request(request.http_request)
|
||||
self._context["request"] = request
|
||||
if not self._user:
|
||||
self._user = request.user
|
||||
self._context["context"] = request.context
|
||||
|
||||
def set_http_request(self, request: HttpRequest):
|
||||
|
@ -0,0 +1,26 @@
|
||||
# Generated by Django 5.0.3 on 2024-03-20 12:14
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_policies_expression", "0004_expressionpolicy_authentik_p_policy__fb6feb_idx"),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="expressionpolicy",
|
||||
name="execution_user",
|
||||
field=models.ForeignKey(
|
||||
default=None,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_DEFAULT,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
]
|
@ -12,6 +12,9 @@ from authentik.policies.types import PolicyRequest, PolicyResult
|
||||
class ExpressionPolicy(Policy):
|
||||
"""Execute arbitrary Python code to implement custom checks and validation."""
|
||||
|
||||
execution_user = models.ForeignKey(
|
||||
"authentik_core.User", default=None, null=True, on_delete=models.SET_DEFAULT
|
||||
)
|
||||
expression = models.TextField()
|
||||
|
||||
@property
|
||||
@ -26,17 +29,11 @@ class ExpressionPolicy(Policy):
|
||||
|
||||
def passes(self, request: PolicyRequest) -> PolicyResult:
|
||||
"""Evaluate and render expression. Returns PolicyResult(false) on error."""
|
||||
evaluator = PolicyEvaluator(self.name)
|
||||
evaluator = PolicyEvaluator(self.execution_user, self.name)
|
||||
evaluator.policy = self
|
||||
evaluator.set_policy_request(request)
|
||||
return evaluator.evaluate(self.expression)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
evaluator = PolicyEvaluator(self.name)
|
||||
evaluator.policy = self
|
||||
evaluator.validate(self.expression)
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
class Meta(Policy.PolicyMeta):
|
||||
verbose_name = _("Expression Policy")
|
||||
verbose_name_plural = _("Expression Policies")
|
||||
|
13
authentik/policies/expression/signals.py
Normal file
13
authentik/policies/expression/signals.py
Normal file
@ -0,0 +1,13 @@
|
||||
from django.db.models.signals import pre_save
|
||||
from django.dispatch import receiver
|
||||
|
||||
from authentik.policies.expression.evaluator import PolicyEvaluator
|
||||
from authentik.policies.expression.models import ExpressionPolicy
|
||||
|
||||
|
||||
@receiver(pre_save, sender=ExpressionPolicy)
|
||||
def pre_save_expression_policy(sender: type[ExpressionPolicy], instance: ExpressionPolicy, **_):
|
||||
"""Ensure policy is valid before saving"""
|
||||
evaluator = PolicyEvaluator(instance.execution_user, instance.name)
|
||||
evaluator.policy = instance
|
||||
evaluator.validate(instance.expression)
|
@ -41,14 +41,14 @@ class TestEvaluator(TestCase):
|
||||
def test_valid(self):
|
||||
"""test simple value expression"""
|
||||
template = "return True"
|
||||
evaluator = PolicyEvaluator("test")
|
||||
evaluator = PolicyEvaluator(self.request.user, "test")
|
||||
evaluator.set_policy_request(self.request)
|
||||
self.assertEqual(evaluator.evaluate(template).passing, True)
|
||||
|
||||
def test_messages(self):
|
||||
"""test expression with message return"""
|
||||
template = 'ak_message("some message");return False'
|
||||
evaluator = PolicyEvaluator("test")
|
||||
evaluator = PolicyEvaluator(self.request.user, "test")
|
||||
evaluator.set_policy_request(self.request)
|
||||
result = evaluator.evaluate(template)
|
||||
self.assertEqual(result.passing, False)
|
||||
@ -57,7 +57,7 @@ class TestEvaluator(TestCase):
|
||||
def test_invalid_syntax(self):
|
||||
"""test invalid syntax"""
|
||||
template = ";"
|
||||
evaluator = PolicyEvaluator("test")
|
||||
evaluator = PolicyEvaluator(self.request.user, "test")
|
||||
evaluator.set_policy_request(self.request)
|
||||
with self.assertRaises(PolicyException):
|
||||
evaluator.evaluate(template)
|
||||
@ -65,14 +65,14 @@ class TestEvaluator(TestCase):
|
||||
def test_validate(self):
|
||||
"""test validate"""
|
||||
template = "True"
|
||||
evaluator = PolicyEvaluator("test")
|
||||
evaluator = PolicyEvaluator(self.request.user, "test")
|
||||
result = evaluator.validate(template)
|
||||
self.assertEqual(result, True)
|
||||
|
||||
def test_validate_invalid(self):
|
||||
"""test validate"""
|
||||
template = ";"
|
||||
evaluator = PolicyEvaluator("test")
|
||||
evaluator = PolicyEvaluator(self.request.user, "test")
|
||||
with self.assertRaises(ValidationError):
|
||||
evaluator.validate(template)
|
||||
|
||||
@ -83,7 +83,7 @@ class TestEvaluator(TestCase):
|
||||
execution_logging=True,
|
||||
expression="ak_message(request.http_request.path)\nreturn True",
|
||||
)
|
||||
evaluator = PolicyEvaluator("test")
|
||||
evaluator = PolicyEvaluator(self.request.user, "test")
|
||||
evaluator.set_policy_request(self.request)
|
||||
proc = PolicyProcess(PolicyBinding(policy=expr), request=self.request, connection=None)
|
||||
res = proc.profiling_wrapper()
|
||||
@ -96,16 +96,13 @@ class TestEvaluator(TestCase):
|
||||
execution_logging=True,
|
||||
expression="ak_message(request.http_request.path)\nreturn True",
|
||||
)
|
||||
tmpl = (
|
||||
"""
|
||||
tmpl = f"""
|
||||
ak_message(request.http_request.path)
|
||||
res = ak_call_policy('%s')
|
||||
res = ak_call_policy('{expr.name}')
|
||||
ak_message(request.http_request.path)
|
||||
for msg in res.messages:
|
||||
ak_message(msg)
|
||||
"""
|
||||
% expr.name
|
||||
)
|
||||
evaluator = PolicyEvaluator("test")
|
||||
evaluator.set_policy_request(self.request)
|
||||
res = evaluator.evaluate(tmpl)
|
||||
|
@ -10,7 +10,6 @@ from jwt import PyJWKSet
|
||||
|
||||
from authentik.core.models import Application
|
||||
from authentik.core.tests.utils import create_test_cert, create_test_flow
|
||||
from authentik.crypto.builder import PrivateKeyAlg
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.providers.oauth2.models import OAuth2Provider
|
||||
@ -83,7 +82,7 @@ class TestJWKS(OAuthTestCase):
|
||||
client_id="test",
|
||||
authorization_flow=create_test_flow(),
|
||||
redirect_uris="http://local.invalid",
|
||||
signing_key=create_test_cert(PrivateKeyAlg.ECDSA),
|
||||
signing_key=create_test_cert(use_ec_private_key=True),
|
||||
)
|
||||
app = Application.objects.create(name="test", slug="test", provider=provider)
|
||||
response = self.client.get(
|
||||
|
@ -1,44 +0,0 @@
|
||||
# Generated by Django 5.0.4 on 2024-05-01 15:32
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_providers_saml", "0013_samlprovider_default_relay_state"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="samlprovider",
|
||||
name="digest_algorithm",
|
||||
field=models.TextField(
|
||||
choices=[
|
||||
("http://www.w3.org/2000/09/xmldsig#sha1", "SHA1"),
|
||||
("http://www.w3.org/2001/04/xmlenc#sha256", "SHA256"),
|
||||
("http://www.w3.org/2001/04/xmldsig-more#sha384", "SHA384"),
|
||||
("http://www.w3.org/2001/04/xmlenc#sha512", "SHA512"),
|
||||
],
|
||||
default="http://www.w3.org/2001/04/xmlenc#sha256",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="samlprovider",
|
||||
name="signature_algorithm",
|
||||
field=models.TextField(
|
||||
choices=[
|
||||
("http://www.w3.org/2000/09/xmldsig#rsa-sha1", "RSA-SHA1"),
|
||||
("http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", "RSA-SHA256"),
|
||||
("http://www.w3.org/2001/04/xmldsig-more#rsa-sha384", "RSA-SHA384"),
|
||||
("http://www.w3.org/2001/04/xmldsig-more#rsa-sha512", "RSA-SHA512"),
|
||||
("http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha1", "ECDSA-SHA1"),
|
||||
("http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256", "ECDSA-SHA256"),
|
||||
("http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha384", "ECDSA-SHA384"),
|
||||
("http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha512", "ECDSA-SHA512"),
|
||||
("http://www.w3.org/2000/09/xmldsig#dsa-sha1", "DSA-SHA1"),
|
||||
],
|
||||
default="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
|
||||
),
|
||||
),
|
||||
]
|
@ -11,10 +11,6 @@ from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.lib.utils.time import timedelta_string_validator
|
||||
from authentik.sources.saml.processors.constants import (
|
||||
DSA_SHA1,
|
||||
ECDSA_SHA1,
|
||||
ECDSA_SHA256,
|
||||
ECDSA_SHA384,
|
||||
ECDSA_SHA512,
|
||||
RSA_SHA1,
|
||||
RSA_SHA256,
|
||||
RSA_SHA384,
|
||||
@ -96,7 +92,8 @@ class SAMLProvider(Provider):
|
||||
),
|
||||
)
|
||||
|
||||
digest_algorithm = models.TextField(
|
||||
digest_algorithm = models.CharField(
|
||||
max_length=50,
|
||||
choices=(
|
||||
(SHA1, _("SHA1")),
|
||||
(SHA256, _("SHA256")),
|
||||
@ -105,16 +102,13 @@ class SAMLProvider(Provider):
|
||||
),
|
||||
default=SHA256,
|
||||
)
|
||||
signature_algorithm = models.TextField(
|
||||
signature_algorithm = models.CharField(
|
||||
max_length=50,
|
||||
choices=(
|
||||
(RSA_SHA1, _("RSA-SHA1")),
|
||||
(RSA_SHA256, _("RSA-SHA256")),
|
||||
(RSA_SHA384, _("RSA-SHA384")),
|
||||
(RSA_SHA512, _("RSA-SHA512")),
|
||||
(ECDSA_SHA1, _("ECDSA-SHA1")),
|
||||
(ECDSA_SHA256, _("ECDSA-SHA256")),
|
||||
(ECDSA_SHA384, _("ECDSA-SHA384")),
|
||||
(ECDSA_SHA512, _("ECDSA-SHA512")),
|
||||
(DSA_SHA1, _("DSA-SHA1")),
|
||||
),
|
||||
default=RSA_SHA256,
|
||||
|
@ -7,14 +7,13 @@ from lxml import etree # nosec
|
||||
|
||||
from authentik.core.models import Application
|
||||
from authentik.core.tests.utils import create_test_cert, create_test_flow
|
||||
from authentik.crypto.builder import PrivateKeyAlg
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.lib.tests.utils import load_fixture
|
||||
from authentik.lib.xml import lxml_from_string
|
||||
from authentik.providers.saml.models import SAMLBindings, SAMLPropertyMapping, SAMLProvider
|
||||
from authentik.providers.saml.processors.metadata import MetadataProcessor
|
||||
from authentik.providers.saml.processors.metadata_parser import ServiceProviderMetadataParser
|
||||
from authentik.sources.saml.processors.constants import ECDSA_SHA256, NS_MAP, NS_SAML_METADATA
|
||||
from authentik.sources.saml.processors.constants import NS_MAP, NS_SAML_METADATA
|
||||
|
||||
|
||||
class TestServiceProviderMetadataParser(TestCase):
|
||||
@ -108,41 +107,12 @@ class TestServiceProviderMetadataParser(TestCase):
|
||||
load_fixture("fixtures/cert.xml").replace("/apps/user_saml", "")
|
||||
)
|
||||
|
||||
def test_signature_rsa(self):
|
||||
"""Test signature validation (RSA)"""
|
||||
def test_signature(self):
|
||||
"""Test signature validation"""
|
||||
provider = SAMLProvider.objects.create(
|
||||
name=generate_id(),
|
||||
authorization_flow=self.flow,
|
||||
signing_kp=create_test_cert(PrivateKeyAlg.RSA),
|
||||
)
|
||||
Application.objects.create(
|
||||
name=generate_id(),
|
||||
slug=generate_id(),
|
||||
provider=provider,
|
||||
)
|
||||
request = self.factory.get("/")
|
||||
metadata = MetadataProcessor(provider, request).build_entity_descriptor()
|
||||
|
||||
root = fromstring(metadata.encode())
|
||||
xmlsec.tree.add_ids(root, ["ID"])
|
||||
signature_nodes = root.xpath("/md:EntityDescriptor/ds:Signature", namespaces=NS_MAP)
|
||||
signature_node = signature_nodes[0]
|
||||
ctx = xmlsec.SignatureContext()
|
||||
key = xmlsec.Key.from_memory(
|
||||
provider.signing_kp.certificate_data,
|
||||
xmlsec.constants.KeyDataFormatCertPem,
|
||||
None,
|
||||
)
|
||||
ctx.key = key
|
||||
ctx.verify(signature_node)
|
||||
|
||||
def test_signature_ecdsa(self):
|
||||
"""Test signature validation (ECDSA)"""
|
||||
provider = SAMLProvider.objects.create(
|
||||
name=generate_id(),
|
||||
authorization_flow=self.flow,
|
||||
signing_kp=create_test_cert(PrivateKeyAlg.ECDSA),
|
||||
signature_algorithm=ECDSA_SHA256,
|
||||
signing_kp=create_test_cert(),
|
||||
)
|
||||
Application.objects.create(
|
||||
name=generate_id(),
|
||||
|
@ -41,7 +41,7 @@ class SCIMGroupClient(SCIMClient[Group, SCIMGroupSchema]):
|
||||
if not scim_group:
|
||||
self.logger.debug("Group does not exist in SCIM, skipping")
|
||||
return None
|
||||
response = self._request("DELETE", f"/Groups/{scim_group.scim_id}")
|
||||
response = self._request("DELETE", f"/Groups/{scim_group.id}")
|
||||
scim_group.delete()
|
||||
return response
|
||||
|
||||
@ -89,7 +89,7 @@ class SCIMGroupClient(SCIMClient[Group, SCIMGroupSchema]):
|
||||
for user in connections:
|
||||
members.append(
|
||||
GroupMember(
|
||||
value=user.scim_id,
|
||||
value=user.id,
|
||||
)
|
||||
)
|
||||
if members:
|
||||
@ -107,19 +107,16 @@ class SCIMGroupClient(SCIMClient[Group, SCIMGroupSchema]):
|
||||
exclude_unset=True,
|
||||
),
|
||||
)
|
||||
scim_id = response.get("id")
|
||||
if not scim_id or scim_id == "":
|
||||
raise StopSync("SCIM Response with missing or invalid `id`")
|
||||
SCIMGroup.objects.create(provider=self.provider, group=group, scim_id=scim_id)
|
||||
SCIMGroup.objects.create(provider=self.provider, group=group, id=response["id"])
|
||||
|
||||
def _update(self, group: Group, connection: SCIMGroup):
|
||||
"""Update existing group"""
|
||||
scim_group = self.to_scim(group)
|
||||
scim_group.id = connection.scim_id
|
||||
scim_group.id = connection.id
|
||||
try:
|
||||
return self._request(
|
||||
"PUT",
|
||||
f"/Groups/{connection.scim_id}",
|
||||
f"/Groups/{scim_group.id}",
|
||||
json=scim_group.model_dump(
|
||||
mode="json",
|
||||
exclude_unset=True,
|
||||
@ -188,13 +185,13 @@ class SCIMGroupClient(SCIMClient[Group, SCIMGroupSchema]):
|
||||
return
|
||||
user_ids = list(
|
||||
SCIMUser.objects.filter(user__pk__in=users_set, provider=self.provider).values_list(
|
||||
"scim_id", flat=True
|
||||
"id", flat=True
|
||||
)
|
||||
)
|
||||
if len(user_ids) < 1:
|
||||
return
|
||||
self._patch(
|
||||
scim_group.scim_id,
|
||||
scim_group.id,
|
||||
PatchOperation(
|
||||
op=PatchOp.add,
|
||||
path="members",
|
||||
@ -214,13 +211,13 @@ class SCIMGroupClient(SCIMClient[Group, SCIMGroupSchema]):
|
||||
return
|
||||
user_ids = list(
|
||||
SCIMUser.objects.filter(user__pk__in=users_set, provider=self.provider).values_list(
|
||||
"scim_id", flat=True
|
||||
"id", flat=True
|
||||
)
|
||||
)
|
||||
if len(user_ids) < 1:
|
||||
return
|
||||
self._patch(
|
||||
scim_group.scim_id,
|
||||
scim_group.id,
|
||||
PatchOperation(
|
||||
op=PatchOp.remove,
|
||||
path="members",
|
||||
|
@ -9,14 +9,13 @@ from pydanticscim.service_provider import (
|
||||
)
|
||||
from pydanticscim.user import User as BaseUser
|
||||
|
||||
SCIM_USER_SCHEMA = "urn:ietf:params:scim:schemas:core:2.0:User"
|
||||
SCIM_GROUP_SCHEMA = "urn:ietf:params:scim:schemas:core:2.0:Group"
|
||||
|
||||
|
||||
class User(BaseUser):
|
||||
"""Modified User schema with added externalId field"""
|
||||
|
||||
schemas: list[str] = [SCIM_USER_SCHEMA]
|
||||
schemas: list[str] = [
|
||||
"urn:ietf:params:scim:schemas:core:2.0:User",
|
||||
]
|
||||
externalId: str | None = None
|
||||
meta: dict | None = None
|
||||
|
||||
@ -24,7 +23,9 @@ class User(BaseUser):
|
||||
class Group(BaseGroup):
|
||||
"""Modified Group schema with added externalId field"""
|
||||
|
||||
schemas: list[str] = [SCIM_GROUP_SCHEMA]
|
||||
schemas: list[str] = [
|
||||
"urn:ietf:params:scim:schemas:core:2.0:Group",
|
||||
]
|
||||
externalId: str | None = None
|
||||
meta: dict | None = None
|
||||
|
||||
|
@ -34,7 +34,7 @@ class SCIMUserClient(SCIMClient[User, SCIMUserSchema]):
|
||||
if not scim_user:
|
||||
self.logger.debug("User does not exist in SCIM, skipping")
|
||||
return None
|
||||
response = self._request("DELETE", f"/Users/{scim_user.scim_id}")
|
||||
response = self._request("DELETE", f"/Users/{scim_user.id}")
|
||||
scim_user.delete()
|
||||
return response
|
||||
|
||||
@ -85,18 +85,15 @@ class SCIMUserClient(SCIMClient[User, SCIMUserSchema]):
|
||||
exclude_unset=True,
|
||||
),
|
||||
)
|
||||
scim_id = response.get("id")
|
||||
if not scim_id or scim_id == "":
|
||||
raise StopSync("SCIM Response with missing or invalid `id`")
|
||||
SCIMUser.objects.create(provider=self.provider, user=user, scim_id=scim_id)
|
||||
SCIMUser.objects.create(provider=self.provider, user=user, id=response["id"])
|
||||
|
||||
def _update(self, user: User, connection: SCIMUser):
|
||||
"""Update existing user"""
|
||||
scim_user = self.to_scim(user)
|
||||
scim_user.id = connection.scim_id
|
||||
scim_user.id = connection.id
|
||||
self._request(
|
||||
"PUT",
|
||||
f"/Users/{connection.scim_id}",
|
||||
f"/Users/{connection.id}",
|
||||
json=scim_user.model_dump(
|
||||
mode="json",
|
||||
exclude_unset=True,
|
||||
|
@ -3,7 +3,7 @@
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.providers.scim.models import SCIMProvider
|
||||
from authentik.providers.scim.tasks import scim_task_wrapper
|
||||
from authentik.providers.scim.tasks import scim_sync
|
||||
from authentik.tenants.management import TenantCommand
|
||||
|
||||
LOGGER = get_logger()
|
||||
@ -21,4 +21,4 @@ class Command(TenantCommand):
|
||||
if not provider:
|
||||
LOGGER.warning("Provider does not exist", name=provider_name)
|
||||
continue
|
||||
scim_task_wrapper(provider.pk).get()
|
||||
scim_sync.delay(provider.pk).get()
|
||||
|
@ -1,76 +0,0 @@
|
||||
# Generated by Django 5.0.4 on 2024-05-03 12:38
|
||||
|
||||
import uuid
|
||||
from django.db import migrations, models
|
||||
from django.apps.registry import Apps
|
||||
|
||||
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||
|
||||
from authentik.lib.migrations import progress_bar
|
||||
|
||||
|
||||
def fix_scim_user_group_pk(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||
SCIMUser = apps.get_model("authentik_providers_scim", "SCIMUser")
|
||||
SCIMGroup = apps.get_model("authentik_providers_scim", "SCIMGroup")
|
||||
db_alias = schema_editor.connection.alias
|
||||
print("\nFixing primary key for SCIM users, this might take a couple of minutes...")
|
||||
for user in progress_bar(SCIMUser.objects.using(db_alias).all()):
|
||||
SCIMUser.objects.using(db_alias).filter(
|
||||
pk=user.pk, user=user.user_id, provider=user.provider_id
|
||||
).update(scim_id=user.pk, id=uuid.uuid4())
|
||||
|
||||
print("\nFixing primary key for SCIM groups, this might take a couple of minutes...")
|
||||
for group in progress_bar(SCIMGroup.objects.using(db_alias).all()):
|
||||
SCIMGroup.objects.using(db_alias).filter(
|
||||
pk=group.pk, group=group.group_id, provider=group.provider_id
|
||||
).update(scim_id=group.pk, id=uuid.uuid4())
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
(
|
||||
"authentik_providers_scim",
|
||||
"0001_squashed_0006_rename_parent_group_scimprovider_filter_group",
|
||||
),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="scimgroup",
|
||||
name="scim_id",
|
||||
field=models.TextField(default="temp"),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="scimuser",
|
||||
name="scim_id",
|
||||
field=models.TextField(default="temp"),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.RunPython(fix_scim_user_group_pk),
|
||||
migrations.AlterField(
|
||||
model_name="scimgroup",
|
||||
name="id",
|
||||
field=models.UUIDField(
|
||||
default=uuid.uuid4, editable=False, primary_key=True, serialize=False
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="scimuser",
|
||||
name="id",
|
||||
field=models.UUIDField(
|
||||
default=uuid.uuid4, editable=False, primary_key=True, serialize=False
|
||||
),
|
||||
),
|
||||
migrations.AlterField(model_name="scimuser", name="scim_id", field=models.TextField()),
|
||||
migrations.AlterField(model_name="scimgroup", name="scim_id", field=models.TextField()),
|
||||
migrations.AlterUniqueTogether(
|
||||
name="scimgroup",
|
||||
unique_together={("scim_id", "group", "provider")},
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name="scimuser",
|
||||
unique_together={("scim_id", "user", "provider")},
|
||||
),
|
||||
]
|
@ -1,7 +1,5 @@
|
||||
"""SCIM Provider models"""
|
||||
|
||||
from uuid import uuid4
|
||||
|
||||
from django.core.cache import cache
|
||||
from django.db import models
|
||||
from django.db.models import QuerySet
|
||||
@ -99,13 +97,12 @@ class SCIMMapping(PropertyMapping):
|
||||
class SCIMUser(models.Model):
|
||||
"""Mapping of a user and provider to a SCIM user ID"""
|
||||
|
||||
id = models.UUIDField(primary_key=True, editable=False, default=uuid4)
|
||||
scim_id = models.TextField()
|
||||
id = models.TextField(primary_key=True)
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
provider = models.ForeignKey(SCIMProvider, on_delete=models.CASCADE)
|
||||
|
||||
class Meta:
|
||||
unique_together = (("scim_id", "user", "provider"),)
|
||||
unique_together = (("id", "user", "provider"),)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"SCIM User {self.user_id} to {self.provider_id}"
|
||||
@ -114,13 +111,12 @@ class SCIMUser(models.Model):
|
||||
class SCIMGroup(models.Model):
|
||||
"""Mapping of a group and provider to a SCIM user ID"""
|
||||
|
||||
id = models.UUIDField(primary_key=True, editable=False, default=uuid4)
|
||||
scim_id = models.TextField()
|
||||
id = models.TextField(primary_key=True)
|
||||
group = models.ForeignKey(Group, on_delete=models.CASCADE)
|
||||
provider = models.ForeignKey(SCIMProvider, on_delete=models.CASCADE)
|
||||
|
||||
class Meta:
|
||||
unique_together = (("scim_id", "group", "provider"),)
|
||||
unique_together = (("id", "group", "provider"),)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"SCIM Group {self.group_id} to {self.provider_id}"
|
||||
|
@ -9,7 +9,7 @@ from structlog.stdlib import get_logger
|
||||
from authentik.core.models import Group, User
|
||||
from authentik.lib.utils.reflection import class_to_path
|
||||
from authentik.providers.scim.models import SCIMProvider
|
||||
from authentik.providers.scim.tasks import scim_signal_direct, scim_signal_m2m, scim_task_wrapper
|
||||
from authentik.providers.scim.tasks import scim_signal_direct, scim_signal_m2m, scim_sync
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
@ -17,7 +17,7 @@ LOGGER = get_logger()
|
||||
@receiver(post_save, sender=SCIMProvider)
|
||||
def post_save_provider(sender: type[Model], instance, created: bool, **_):
|
||||
"""Trigger sync when SCIM provider is saved"""
|
||||
scim_task_wrapper(instance.pk)
|
||||
scim_sync.delay(instance.pk)
|
||||
|
||||
|
||||
@receiver(post_save, sender=User)
|
||||
|
@ -38,23 +38,7 @@ def client_for_model(provider: SCIMProvider, model: Model) -> SCIMClient:
|
||||
def scim_sync_all():
|
||||
"""Run sync for all providers"""
|
||||
for provider in SCIMProvider.objects.filter(backchannel_application__isnull=False):
|
||||
scim_task_wrapper(provider.pk)
|
||||
|
||||
|
||||
def scim_task_wrapper(provider_pk: int):
|
||||
"""Wrap scim_sync to set the correct timeouts"""
|
||||
provider: SCIMProvider = SCIMProvider.objects.filter(
|
||||
pk=provider_pk, backchannel_application__isnull=False
|
||||
).first()
|
||||
if not provider:
|
||||
return
|
||||
users_paginator = Paginator(provider.get_user_qs(), PAGE_SIZE)
|
||||
groups_paginator = Paginator(provider.get_group_qs(), PAGE_SIZE)
|
||||
soft_time_limit = (users_paginator.num_pages + groups_paginator.num_pages) * PAGE_TIMEOUT
|
||||
time_limit = soft_time_limit * 1.5
|
||||
return scim_sync.apply_async(
|
||||
(provider.pk,), time_limit=int(time_limit), soft_time_limit=int(soft_time_limit)
|
||||
)
|
||||
scim_sync.delay(provider.pk)
|
||||
|
||||
|
||||
@CELERY_APP.task(bind=True, base=SystemTask)
|
||||
@ -76,7 +60,7 @@ def scim_sync(self: SystemTask, provider_pk: int) -> None:
|
||||
users_paginator = Paginator(provider.get_user_qs(), PAGE_SIZE)
|
||||
groups_paginator = Paginator(provider.get_group_qs(), PAGE_SIZE)
|
||||
self.soft_time_limit = self.time_limit = (
|
||||
users_paginator.num_pages + groups_paginator.num_pages
|
||||
users_paginator.count + groups_paginator.count
|
||||
) * PAGE_TIMEOUT
|
||||
with allow_join_result():
|
||||
try:
|
||||
|
@ -8,7 +8,7 @@ from authentik.core.models import Application, Group, User
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.providers.scim.clients.schema import ServiceProviderConfiguration
|
||||
from authentik.providers.scim.models import SCIMMapping, SCIMProvider
|
||||
from authentik.providers.scim.tasks import scim_task_wrapper
|
||||
from authentik.providers.scim.tasks import scim_sync
|
||||
from authentik.tenants.models import Tenant
|
||||
|
||||
|
||||
@ -79,7 +79,7 @@ class SCIMMembershipTests(TestCase):
|
||||
)
|
||||
|
||||
self.configure()
|
||||
scim_task_wrapper(self.provider.pk).get()
|
||||
scim_sync.delay(self.provider.pk).get()
|
||||
|
||||
self.assertEqual(mocker.call_count, 6)
|
||||
self.assertEqual(mocker.request_history[0].method, "GET")
|
||||
@ -169,7 +169,7 @@ class SCIMMembershipTests(TestCase):
|
||||
)
|
||||
|
||||
self.configure()
|
||||
scim_task_wrapper(self.provider.pk).get()
|
||||
scim_sync.delay(self.provider.pk).get()
|
||||
|
||||
self.assertEqual(mocker.call_count, 6)
|
||||
self.assertEqual(mocker.request_history[0].method, "GET")
|
||||
|
@ -10,7 +10,7 @@ from authentik.blueprints.tests import apply_blueprint
|
||||
from authentik.core.models import Application, Group, User
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.providers.scim.models import SCIMMapping, SCIMProvider
|
||||
from authentik.providers.scim.tasks import scim_task_wrapper
|
||||
from authentik.providers.scim.tasks import scim_sync
|
||||
from authentik.tenants.models import Tenant
|
||||
|
||||
|
||||
@ -88,72 +88,6 @@ class SCIMUserTests(TestCase):
|
||||
},
|
||||
)
|
||||
|
||||
@Mocker()
|
||||
def test_user_create_different_provider_same_id(self, mock: Mocker):
|
||||
"""Test user creation with multiple providers that happen
|
||||
to return the same object ID"""
|
||||
# Create duplicate provider
|
||||
provider: SCIMProvider = SCIMProvider.objects.create(
|
||||
name=generate_id(),
|
||||
url="https://localhost",
|
||||
token=generate_id(),
|
||||
exclude_users_service_account=True,
|
||||
)
|
||||
app: Application = Application.objects.create(
|
||||
name=generate_id(),
|
||||
slug=generate_id(),
|
||||
)
|
||||
app.backchannel_providers.add(provider)
|
||||
provider.property_mappings.add(
|
||||
SCIMMapping.objects.get(managed="goauthentik.io/providers/scim/user")
|
||||
)
|
||||
provider.property_mappings_group.add(
|
||||
SCIMMapping.objects.get(managed="goauthentik.io/providers/scim/group")
|
||||
)
|
||||
|
||||
scim_id = generate_id()
|
||||
mock.get(
|
||||
"https://localhost/ServiceProviderConfig",
|
||||
json={},
|
||||
)
|
||||
mock.post(
|
||||
"https://localhost/Users",
|
||||
json={
|
||||
"id": scim_id,
|
||||
},
|
||||
)
|
||||
uid = generate_id()
|
||||
user = User.objects.create(
|
||||
username=uid,
|
||||
name=f"{uid} {uid}",
|
||||
email=f"{uid}@goauthentik.io",
|
||||
)
|
||||
self.assertEqual(mock.call_count, 4)
|
||||
self.assertEqual(mock.request_history[0].method, "GET")
|
||||
self.assertEqual(mock.request_history[1].method, "POST")
|
||||
self.assertJSONEqual(
|
||||
mock.request_history[1].body,
|
||||
{
|
||||
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
|
||||
"active": True,
|
||||
"emails": [
|
||||
{
|
||||
"primary": True,
|
||||
"type": "other",
|
||||
"value": f"{uid}@goauthentik.io",
|
||||
}
|
||||
],
|
||||
"externalId": user.uid,
|
||||
"name": {
|
||||
"familyName": uid,
|
||||
"formatted": f"{uid} {uid}",
|
||||
"givenName": uid,
|
||||
},
|
||||
"displayName": f"{uid} {uid}",
|
||||
"userName": uid,
|
||||
},
|
||||
)
|
||||
|
||||
@Mocker()
|
||||
def test_user_create_update(self, mock: Mocker):
|
||||
"""Test user creation and update"""
|
||||
@ -302,7 +236,7 @@ class SCIMUserTests(TestCase):
|
||||
email=f"{uid}@goauthentik.io",
|
||||
)
|
||||
|
||||
scim_task_wrapper(self.provider.pk).get()
|
||||
scim_sync.delay(self.provider.pk).get()
|
||||
|
||||
self.assertEqual(mock.call_count, 5)
|
||||
self.assertEqual(mock.request_history[0].method, "GET")
|
||||
|
@ -376,13 +376,7 @@ CELERY = {
|
||||
"task_default_queue": "authentik",
|
||||
"broker_url": CONFIG.get("broker.url") or redis_url(CONFIG.get("redis.db")),
|
||||
"result_backend": CONFIG.get("result_backend.url") or redis_url(CONFIG.get("redis.db")),
|
||||
"broker_transport_options": CONFIG.get_dict_from_b64_json(
|
||||
"broker.transport_options", {"retry_policy": {"timeout": 5.0}}
|
||||
),
|
||||
"result_backend_transport_options": CONFIG.get_dict_from_b64_json(
|
||||
"result_backend.transport_options", {"retry_policy": {"timeout": 5.0}}
|
||||
),
|
||||
"redis_retry_on_timeout": True,
|
||||
"broker_transport_options": CONFIG.get_dict_from_b64_json("broker.transport_options"),
|
||||
}
|
||||
|
||||
# Sentry integration
|
||||
|
@ -76,7 +76,7 @@ class S3Storage(BaseS3Storage):
|
||||
|
||||
return safe_join(self.location, connection.schema_name, name)
|
||||
except ValueError:
|
||||
raise SuspiciousOperation("Attempted access to '%s' denied." % name) from None
|
||||
raise SuspiciousOperation(f"Attempted access to '{name}' denied.") from None
|
||||
|
||||
# This is a fix for https://github.com/jschneier/django-storages/pull/839
|
||||
def url(self, name, parameters=None, expire=None, http_method=None):
|
||||
|
@ -1,44 +0,0 @@
|
||||
# Generated by Django 5.0.4 on 2024-05-01 15:44
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_sources_saml", "0013_samlsource_verification_kp_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="samlsource",
|
||||
name="digest_algorithm",
|
||||
field=models.TextField(
|
||||
choices=[
|
||||
("http://www.w3.org/2000/09/xmldsig#sha1", "SHA1"),
|
||||
("http://www.w3.org/2001/04/xmlenc#sha256", "SHA256"),
|
||||
("http://www.w3.org/2001/04/xmldsig-more#sha384", "SHA384"),
|
||||
("http://www.w3.org/2001/04/xmlenc#sha512", "SHA512"),
|
||||
],
|
||||
default="http://www.w3.org/2001/04/xmlenc#sha256",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="samlsource",
|
||||
name="signature_algorithm",
|
||||
field=models.TextField(
|
||||
choices=[
|
||||
("http://www.w3.org/2000/09/xmldsig#rsa-sha1", "RSA-SHA1"),
|
||||
("http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", "RSA-SHA256"),
|
||||
("http://www.w3.org/2001/04/xmldsig-more#rsa-sha384", "RSA-SHA384"),
|
||||
("http://www.w3.org/2001/04/xmldsig-more#rsa-sha512", "RSA-SHA512"),
|
||||
("http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha1", "ECDSA-SHA1"),
|
||||
("http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256", "ECDSA-SHA256"),
|
||||
("http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha384", "ECDSA-SHA384"),
|
||||
("http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha512", "ECDSA-SHA512"),
|
||||
("http://www.w3.org/2000/09/xmldsig#dsa-sha1", "DSA-SHA1"),
|
||||
],
|
||||
default="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
|
||||
),
|
||||
),
|
||||
]
|
@ -15,10 +15,6 @@ from authentik.flows.models import Flow
|
||||
from authentik.lib.utils.time import timedelta_string_validator
|
||||
from authentik.sources.saml.processors.constants import (
|
||||
DSA_SHA1,
|
||||
ECDSA_SHA1,
|
||||
ECDSA_SHA256,
|
||||
ECDSA_SHA384,
|
||||
ECDSA_SHA512,
|
||||
RSA_SHA1,
|
||||
RSA_SHA256,
|
||||
RSA_SHA384,
|
||||
@ -147,7 +143,8 @@ class SAMLSource(Source):
|
||||
verbose_name=_("Signing Keypair"),
|
||||
)
|
||||
|
||||
digest_algorithm = models.TextField(
|
||||
digest_algorithm = models.CharField(
|
||||
max_length=50,
|
||||
choices=(
|
||||
(SHA1, _("SHA1")),
|
||||
(SHA256, _("SHA256")),
|
||||
@ -156,16 +153,13 @@ class SAMLSource(Source):
|
||||
),
|
||||
default=SHA256,
|
||||
)
|
||||
signature_algorithm = models.TextField(
|
||||
signature_algorithm = models.CharField(
|
||||
max_length=50,
|
||||
choices=(
|
||||
(RSA_SHA1, _("RSA-SHA1")),
|
||||
(RSA_SHA256, _("RSA-SHA256")),
|
||||
(RSA_SHA384, _("RSA-SHA384")),
|
||||
(RSA_SHA512, _("RSA-SHA512")),
|
||||
(ECDSA_SHA1, _("ECDSA-SHA1")),
|
||||
(ECDSA_SHA256, _("ECDSA-SHA256")),
|
||||
(ECDSA_SHA384, _("ECDSA-SHA384")),
|
||||
(ECDSA_SHA512, _("ECDSA-SHA512")),
|
||||
(DSA_SHA1, _("DSA-SHA1")),
|
||||
),
|
||||
default=RSA_SHA256,
|
||||
|
@ -26,16 +26,9 @@ SAML_BINDING_REDIRECT = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
|
||||
|
||||
DSA_SHA1 = "http://www.w3.org/2000/09/xmldsig#dsa-sha1"
|
||||
RSA_SHA1 = "http://www.w3.org/2000/09/xmldsig#rsa-sha1"
|
||||
# https://datatracker.ietf.org/doc/html/rfc4051#section-2.3.2
|
||||
RSA_SHA256 = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"
|
||||
RSA_SHA384 = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha384"
|
||||
RSA_SHA512 = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512"
|
||||
# https://datatracker.ietf.org/doc/html/rfc4051#section-2.3.6
|
||||
ECDSA_SHA1 = "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha1"
|
||||
ECDSA_SHA224 = "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha224"
|
||||
ECDSA_SHA256 = "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256"
|
||||
ECDSA_SHA384 = "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha384"
|
||||
ECDSA_SHA512 = "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha512"
|
||||
|
||||
SHA1 = "http://www.w3.org/2000/09/xmldsig#sha1"
|
||||
SHA256 = "http://www.w3.org/2001/04/xmlenc#sha256"
|
||||
@ -48,11 +41,6 @@ SIGN_ALGORITHM_TRANSFORM_MAP = {
|
||||
RSA_SHA256: xmlsec.constants.TransformRsaSha256,
|
||||
RSA_SHA384: xmlsec.constants.TransformRsaSha384,
|
||||
RSA_SHA512: xmlsec.constants.TransformRsaSha512,
|
||||
ECDSA_SHA1: xmlsec.constants.TransformEcdsaSha1,
|
||||
ECDSA_SHA224: xmlsec.constants.TransformEcdsaSha224,
|
||||
ECDSA_SHA256: xmlsec.constants.TransformEcdsaSha256,
|
||||
ECDSA_SHA384: xmlsec.constants.TransformEcdsaSha384,
|
||||
ECDSA_SHA512: xmlsec.constants.TransformEcdsaSha512,
|
||||
}
|
||||
|
||||
DIGEST_ALGORITHM_TRANSLATION_MAP = {
|
||||
|
@ -13,7 +13,6 @@ from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
|
||||
from authentik.core.models import Group, User
|
||||
from authentik.providers.scim.clients.schema import SCIM_USER_SCHEMA
|
||||
from authentik.providers.scim.clients.schema import Group as SCIMGroupModel
|
||||
from authentik.sources.scim.models import SCIMSourceGroup
|
||||
from authentik.sources.scim.views.v2.base import SCIMView
|
||||
@ -27,11 +26,9 @@ class GroupsView(SCIMView):
|
||||
def group_to_scim(self, scim_group: SCIMSourceGroup) -> dict:
|
||||
"""Convert Group to SCIM data"""
|
||||
payload = SCIMGroupModel(
|
||||
schemas=[SCIM_USER_SCHEMA],
|
||||
id=str(scim_group.group.pk),
|
||||
externalId=scim_group.id,
|
||||
displayName=scim_group.group.name,
|
||||
members=[],
|
||||
meta={
|
||||
"resourceType": "Group",
|
||||
"location": self.request.build_absolute_uri(
|
||||
@ -45,24 +42,28 @@ class GroupsView(SCIMView):
|
||||
),
|
||||
},
|
||||
)
|
||||
for member in scim_group.group.users.order_by("pk"):
|
||||
member: User
|
||||
payload.members.append(GroupMember(value=str(member.uuid)))
|
||||
return payload.model_dump(mode="json", exclude_unset=True)
|
||||
return payload.model_dump(
|
||||
mode="json",
|
||||
exclude_unset=True,
|
||||
)
|
||||
|
||||
def get(self, request: Request, group_id: str | None = None, **kwargs) -> Response:
|
||||
"""List Group handler"""
|
||||
base_query = SCIMSourceGroup.objects.select_related("group").prefetch_related(
|
||||
"group__users"
|
||||
)
|
||||
if group_id:
|
||||
connection = base_query.filter(source=self.source, group__group_uuid=group_id).first()
|
||||
connection = (
|
||||
SCIMSourceGroup.objects.filter(source=self.source, group__group_uuid=group_id)
|
||||
.select_related("group")
|
||||
.first()
|
||||
)
|
||||
if not connection:
|
||||
raise Http404
|
||||
return Response(self.group_to_scim(connection))
|
||||
connections = (
|
||||
base_query.filter(source=self.source).order_by("pk").filter(self.filter_parse(request))
|
||||
SCIMSourceGroup.objects.filter(source=self.source)
|
||||
.select_related("group")
|
||||
.order_by("pk")
|
||||
)
|
||||
connections = connections.filter(self.filter_parse(request))
|
||||
page = self.paginate_query(connections)
|
||||
return Response(
|
||||
{
|
||||
@ -78,8 +79,6 @@ class GroupsView(SCIMView):
|
||||
def update_group(self, connection: SCIMSourceGroup | None, data: QueryDict):
|
||||
"""Partial update a group"""
|
||||
group = connection.group if connection else Group()
|
||||
if _group := Group.objects.filter(name=data.get("displayName")).first():
|
||||
group = _group
|
||||
if "displayName" in data:
|
||||
group.name = data.get("displayName")
|
||||
if group.name == "":
|
||||
|
@ -11,7 +11,6 @@ from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
|
||||
from authentik.core.models import User
|
||||
from authentik.providers.scim.clients.schema import SCIM_USER_SCHEMA
|
||||
from authentik.providers.scim.clients.schema import User as SCIMUserModel
|
||||
from authentik.sources.scim.models import SCIMSourceUser
|
||||
from authentik.sources.scim.views.v2.base import SCIMView
|
||||
@ -34,7 +33,6 @@ class UsersView(SCIMView):
|
||||
def user_to_scim(self, scim_user: SCIMSourceUser) -> dict:
|
||||
"""Convert User to SCIM data"""
|
||||
payload = SCIMUserModel(
|
||||
schemas=[SCIM_USER_SCHEMA],
|
||||
id=str(scim_user.user.uuid),
|
||||
externalId=scim_user.id,
|
||||
userName=scim_user.user.username,
|
||||
@ -64,7 +62,10 @@ class UsersView(SCIMView):
|
||||
),
|
||||
},
|
||||
)
|
||||
final_payload = payload.model_dump(mode="json", exclude_unset=True)
|
||||
final_payload = payload.model_dump(
|
||||
mode="json",
|
||||
exclude_unset=True,
|
||||
)
|
||||
final_payload.update(scim_user.attributes)
|
||||
return final_payload
|
||||
|
||||
@ -98,8 +99,6 @@ class UsersView(SCIMView):
|
||||
def update_user(self, connection: SCIMSourceUser | None, data: QueryDict):
|
||||
"""Partial update a user"""
|
||||
user = connection.user if connection else User()
|
||||
if _user := User.objects.filter(username=data.get("userName")).first():
|
||||
user = _user
|
||||
user.path = self.source.get_user_path()
|
||||
if "userName" in data:
|
||||
user.username = data.get("userName")
|
||||
|
@ -1,23 +0,0 @@
|
||||
# Generated by Django 5.0.4 on 2024-05-01 15:32
|
||||
|
||||
import authentik.lib.utils.time
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_tenants", "0002_tenant_default_token_duration_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="tenant",
|
||||
name="default_token_duration",
|
||||
field=models.TextField(
|
||||
default="days=1",
|
||||
help_text="Default token duration",
|
||||
validators=[authentik.lib.utils.time.timedelta_string_validator],
|
||||
),
|
||||
),
|
||||
]
|
@ -3,7 +3,6 @@
|
||||
from tenant_schemas_celery.scheduler import (
|
||||
TenantAwarePersistentScheduler as BaseTenantAwarePersistentScheduler,
|
||||
)
|
||||
from tenant_schemas_celery.scheduler import TenantAwareScheduleEntry
|
||||
|
||||
|
||||
class TenantAwarePersistentScheduler(BaseTenantAwarePersistentScheduler):
|
||||
@ -12,11 +11,3 @@ class TenantAwarePersistentScheduler(BaseTenantAwarePersistentScheduler):
|
||||
@classmethod
|
||||
def get_queryset(cls):
|
||||
return super().get_queryset().filter(ready=True)
|
||||
|
||||
def apply_entry(self, entry: TenantAwareScheduleEntry, producer=None):
|
||||
# https://github.com/maciej-gol/tenant-schemas-celery/blob/master/tenant_schemas_celery/scheduler.py#L85
|
||||
# When (as by default) no tenant schemas are set, the public schema is excluded
|
||||
# so we need to explicitly include it here, otherwise the task is not executed
|
||||
if entry.tenant_schemas is None:
|
||||
entry.tenant_schemas = self.get_queryset().values_list("schema_name", flat=True)
|
||||
return super().apply_entry(entry, producer)
|
||||
|
@ -85,30 +85,19 @@ entries:
|
||||
model: authentik_stages_prompt.prompt
|
||||
- attrs:
|
||||
expression: |
|
||||
from authentik.core.models import (
|
||||
USER_ATTRIBUTE_CHANGE_EMAIL,
|
||||
USER_ATTRIBUTE_CHANGE_NAME,
|
||||
USER_ATTRIBUTE_CHANGE_USERNAME
|
||||
)
|
||||
prompt_data = request.context.get("prompt_data")
|
||||
|
||||
if not request.user.group_attributes(request.http_request).get(
|
||||
USER_ATTRIBUTE_CHANGE_EMAIL, request.http_request.tenant.default_user_change_email
|
||||
):
|
||||
user_group_attributes = request.user.group_attributes(request.http_request)
|
||||
if not user_group_attributes.get(USER_ATTRIBUTE_CHANGE_EMAIL, request.http_request.tenant.default_user_change_email):
|
||||
if prompt_data.get("email") != request.user.email:
|
||||
ak_message("Not allowed to change email address.")
|
||||
return False
|
||||
|
||||
if not request.user.group_attributes(request.http_request).get(
|
||||
USER_ATTRIBUTE_CHANGE_NAME, request.http_request.tenant.default_user_change_name
|
||||
):
|
||||
if not user_group_attributes.get(USER_ATTRIBUTE_CHANGE_NAME, request.http_request.tenant.default_user_change_name):
|
||||
if prompt_data.get("name") != request.user.name:
|
||||
ak_message("Not allowed to change name.")
|
||||
return False
|
||||
|
||||
if not request.user.group_attributes(request.http_request).get(
|
||||
USER_ATTRIBUTE_CHANGE_USERNAME, request.http_request.tenant.default_user_change_username
|
||||
):
|
||||
if not user_group_attributes.get(USER_ATTRIBUTE_CHANGE_USERNAME, request.http_request.tenant.default_user_change_username):
|
||||
if prompt_data.get("username") != request.user.username:
|
||||
ak_message("Not allowed to change username.")
|
||||
return False
|
||||
|
@ -89,10 +89,9 @@ entries:
|
||||
expression: |
|
||||
# This policy ensures that the setup flow can only be
|
||||
# used one time
|
||||
from authentik.flows.models import Flow, FlowAuthenticationRequirement
|
||||
Flow.objects.filter(slug="initial-setup").update(
|
||||
authentication=FlowAuthenticationRequirement.REQUIRE_SUPERUSER,
|
||||
)
|
||||
FlowsApi(api).flows_instances_partial_update("initial-setup", {
|
||||
"authentication": "REQUIRE_SUPERUSER"
|
||||
})
|
||||
return True
|
||||
id: policy-default-oobe-flow-set-authentication
|
||||
identifiers:
|
||||
|
@ -2,7 +2,7 @@
|
||||
"$schema": "http://json-schema.org/draft-07/schema",
|
||||
"$id": "https://goauthentik.io/blueprints/schema.json",
|
||||
"type": "object",
|
||||
"title": "authentik 2024.4.2 Blueprint schema",
|
||||
"title": "authentik 2024.4.1 Blueprint schema",
|
||||
"required": [
|
||||
"version",
|
||||
"entries"
|
||||
@ -3477,6 +3477,10 @@
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"title": "Expression"
|
||||
},
|
||||
"execution_user": {
|
||||
"type": "integer",
|
||||
"title": "Execution user"
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
@ -4131,10 +4135,6 @@
|
||||
"http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
|
||||
"http://www.w3.org/2001/04/xmldsig-more#rsa-sha384",
|
||||
"http://www.w3.org/2001/04/xmldsig-more#rsa-sha512",
|
||||
"http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha1",
|
||||
"http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256",
|
||||
"http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha384",
|
||||
"http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha512",
|
||||
"http://www.w3.org/2000/09/xmldsig#dsa-sha1"
|
||||
],
|
||||
"title": "Signature algorithm"
|
||||
@ -4939,10 +4939,6 @@
|
||||
"http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
|
||||
"http://www.w3.org/2001/04/xmldsig-more#rsa-sha384",
|
||||
"http://www.w3.org/2001/04/xmldsig-more#rsa-sha512",
|
||||
"http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha1",
|
||||
"http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256",
|
||||
"http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha384",
|
||||
"http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha512",
|
||||
"http://www.w3.org/2000/09/xmldsig#dsa-sha1"
|
||||
],
|
||||
"title": "Signature algorithm"
|
||||
|
@ -32,7 +32,7 @@ services:
|
||||
volumes:
|
||||
- redis:/data
|
||||
server:
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.4.2}
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.4.1}
|
||||
restart: unless-stopped
|
||||
command: server
|
||||
environment:
|
||||
@ -53,7 +53,7 @@ services:
|
||||
- postgresql
|
||||
- redis
|
||||
worker:
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.4.2}
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.4.1}
|
||||
restart: unless-stopped
|
||||
command: worker
|
||||
environment:
|
||||
|
2
go.mod
2
go.mod
@ -28,7 +28,7 @@ require (
|
||||
github.com/spf13/cobra v1.8.0
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/wwt/guac v1.3.2
|
||||
goauthentik.io/api/v3 v3.2024023.2
|
||||
goauthentik.io/api/v3 v3.2024041.1
|
||||
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
|
||||
golang.org/x/oauth2 v0.19.0
|
||||
golang.org/x/sync v0.7.0
|
||||
|
4
go.sum
4
go.sum
@ -294,8 +294,8 @@ 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.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
|
||||
go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4=
|
||||
goauthentik.io/api/v3 v3.2024023.2 h1:lSVaZAKTpsDhtw11wnkGjPalkDzv9H2VKEJllBi2aXs=
|
||||
goauthentik.io/api/v3 v3.2024023.2/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
|
||||
goauthentik.io/api/v3 v3.2024041.1 h1:oYj6DYqmZJd6/wyknBZLnLa+4+ShT4ry7HQn0W8VXxY=
|
||||
goauthentik.io/api/v3 v3.2024041.1/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-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
|
@ -29,4 +29,4 @@ func UserAgent() string {
|
||||
return fmt.Sprintf("authentik@%s", FullVersion())
|
||||
}
|
||||
|
||||
const VERSION = "2024.4.2"
|
||||
const VERSION = "2024.4.1"
|
||||
|
@ -117,8 +117,6 @@ def run_migrations():
|
||||
)
|
||||
finally:
|
||||
release_lock(curr)
|
||||
curr.close()
|
||||
conn.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
Binary file not shown.
BIN
locale/ru/LC_MESSAGES/django.mo
Normal file
BIN
locale/ru/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
3205
locale/ru/LC_MESSAGES/django.po
Normal file
3205
locale/ru/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
Binary file not shown.
2342
poetry.lock
generated
2342
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "authentik"
|
||||
version = "2024.4.2"
|
||||
version = "2024.4.1"
|
||||
description = ""
|
||||
authors = ["authentik Team <hello@goauthentik.io>"]
|
||||
|
||||
@ -83,13 +83,13 @@ filterwarnings = [
|
||||
]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
authentik_client = "2024.4.1.post1714149882"
|
||||
argon2-cffi = "*"
|
||||
celery = "*"
|
||||
channels = { version = "*", extras = ["daphne"] }
|
||||
channels-redis = "*"
|
||||
codespell = "*"
|
||||
colorama = "*"
|
||||
cryptography = "*"
|
||||
dacite = "*"
|
||||
deepmerge = "*"
|
||||
defusedxml = "*"
|
||||
@ -102,7 +102,7 @@ django-redis = "*"
|
||||
django-storages = { extras = ["s3"], version = "*" }
|
||||
# See https://github.com/django-tenants/django-tenants/pull/997
|
||||
django-tenants = { git = "https://github.com/rissson/django-tenants.git", branch="authentik-fixes" }
|
||||
djangorestframework = "3.14.0"
|
||||
djangorestframework = "*"
|
||||
djangorestframework-guardian = "*"
|
||||
docker = "*"
|
||||
drf-spectacular = "*"
|
||||
@ -116,17 +116,24 @@ gunicorn = "*"
|
||||
jsonpatch = "*"
|
||||
kubernetes = "*"
|
||||
ldap3 = "*"
|
||||
lxml = "*"
|
||||
lxml = [
|
||||
# 5.0.0 works with libxml2 2.11.x, which is standard on brew
|
||||
{ version = "5.0.0", platform = "darwin" },
|
||||
# 4.9.x works with previous libxml2 versions, which is what we get on linux
|
||||
{ version = "4.9.4", platform = "linux" },
|
||||
]
|
||||
opencontainers = { extras = ["reggie"], version = "*" }
|
||||
packaging = "*"
|
||||
paramiko = "*"
|
||||
psycopg = { extras = ["c"], version = "*" }
|
||||
pycryptodome = "*"
|
||||
pydantic = "*"
|
||||
pydantic-scim = "*"
|
||||
pyjwt = "*"
|
||||
python = "~3.12"
|
||||
pyyaml = "*"
|
||||
requests-oauthlib = "*"
|
||||
restrictedpython = "*"
|
||||
scim2-filter-parser = "*"
|
||||
sentry-sdk = "*"
|
||||
service_identity = "*"
|
||||
|
36
schema.yml
36
schema.yml
@ -1,7 +1,7 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: authentik
|
||||
version: 2024.4.2
|
||||
version: 2024.4.1
|
||||
description: Making authentication simple.
|
||||
contact:
|
||||
email: hello@goauthentik.io
|
||||
@ -11852,6 +11852,10 @@ paths:
|
||||
name: execution_logging
|
||||
schema:
|
||||
type: boolean
|
||||
- in: query
|
||||
name: execution_user
|
||||
schema:
|
||||
type: integer
|
||||
- in: query
|
||||
name: expression
|
||||
schema:
|
||||
@ -17051,10 +17055,6 @@ paths:
|
||||
enum:
|
||||
- http://www.w3.org/2000/09/xmldsig#dsa-sha1
|
||||
- http://www.w3.org/2000/09/xmldsig#rsa-sha1
|
||||
- http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha1
|
||||
- http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256
|
||||
- http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha384
|
||||
- http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha512
|
||||
- http://www.w3.org/2001/04/xmldsig-more#rsa-sha256
|
||||
- http://www.w3.org/2001/04/xmldsig-more#rsa-sha384
|
||||
- http://www.w3.org/2001/04/xmldsig-more#rsa-sha512
|
||||
@ -20914,10 +20914,6 @@ paths:
|
||||
enum:
|
||||
- http://www.w3.org/2000/09/xmldsig#dsa-sha1
|
||||
- http://www.w3.org/2000/09/xmldsig#rsa-sha1
|
||||
- http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha1
|
||||
- http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256
|
||||
- http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha384
|
||||
- http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha512
|
||||
- http://www.w3.org/2001/04/xmldsig-more#rsa-sha256
|
||||
- http://www.w3.org/2001/04/xmldsig-more#rsa-sha384
|
||||
- http://www.w3.org/2001/04/xmldsig-more#rsa-sha512
|
||||
@ -30458,11 +30454,6 @@ components:
|
||||
- pending_user
|
||||
- pending_user_avatar
|
||||
- type
|
||||
AlgEnum:
|
||||
enum:
|
||||
- rsa
|
||||
- ecdsa
|
||||
type: string
|
||||
App:
|
||||
type: object
|
||||
description: Serialize Application info
|
||||
@ -32120,10 +32111,6 @@ components:
|
||||
type: string
|
||||
validity_days:
|
||||
type: integer
|
||||
alg:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/AlgEnum'
|
||||
default: rsa
|
||||
required:
|
||||
- common_name
|
||||
- validity_days
|
||||
@ -33582,6 +33569,9 @@ components:
|
||||
readOnly: true
|
||||
expression:
|
||||
type: string
|
||||
execution_user:
|
||||
type: integer
|
||||
nullable: true
|
||||
required:
|
||||
- bound_to
|
||||
- component
|
||||
@ -33605,6 +33595,9 @@ components:
|
||||
expression:
|
||||
type: string
|
||||
minLength: 1
|
||||
execution_user:
|
||||
type: integer
|
||||
nullable: true
|
||||
required:
|
||||
- expression
|
||||
- name
|
||||
@ -38858,6 +38851,9 @@ components:
|
||||
expression:
|
||||
type: string
|
||||
minLength: 1
|
||||
execution_user:
|
||||
type: integer
|
||||
nullable: true
|
||||
PatchedFlowRequest:
|
||||
type: object
|
||||
description: Flow Serializer
|
||||
@ -43675,10 +43671,6 @@ components:
|
||||
- http://www.w3.org/2001/04/xmldsig-more#rsa-sha256
|
||||
- http://www.w3.org/2001/04/xmldsig-more#rsa-sha384
|
||||
- http://www.w3.org/2001/04/xmldsig-more#rsa-sha512
|
||||
- http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha1
|
||||
- http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256
|
||||
- http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha384
|
||||
- http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha512
|
||||
- http://www.w3.org/2000/09/xmldsig#dsa-sha1
|
||||
type: string
|
||||
Source:
|
||||
|
146
tests/wdio/package-lock.json
generated
146
tests/wdio/package-lock.json
generated
@ -6,16 +6,16 @@
|
||||
"": {
|
||||
"name": "@goauthentik/web-tests",
|
||||
"dependencies": {
|
||||
"chromedriver": "^123.0.4"
|
||||
"chromedriver": "^124.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "^7.5.0",
|
||||
"@typescript-eslint/parser": "^7.5.0",
|
||||
"@wdio/cli": "^8.36.0",
|
||||
"@wdio/local-runner": "^8.36.0",
|
||||
"@wdio/mocha-framework": "^8.36.0",
|
||||
"@wdio/spec-reporter": "^8.36.0",
|
||||
"@wdio/cli": "^8.36.1",
|
||||
"@wdio/local-runner": "^8.36.1",
|
||||
"@wdio/mocha-framework": "^8.36.1",
|
||||
"@wdio/spec-reporter": "^8.36.1",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"eslint-plugin-sonarjs": "^0.25.1",
|
||||
@ -1189,19 +1189,19 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@wdio/cli": {
|
||||
"version": "8.36.0",
|
||||
"resolved": "https://registry.npmjs.org/@wdio/cli/-/cli-8.36.0.tgz",
|
||||
"integrity": "sha512-B8iEwz9DRzHquPihT74nKUzN9s+rCd1TkBp+JGmdgm7pJqiWTe4FORrzaxWjdiCO78jbYK9LgaMORpCcAzjwIA==",
|
||||
"version": "8.36.1",
|
||||
"resolved": "https://registry.npmjs.org/@wdio/cli/-/cli-8.36.1.tgz",
|
||||
"integrity": "sha512-LZBZiwcvvv5P0HuRXt8IV09UiFT5dnDr1Ag5u2roJL2D7l8wDHHa70PXw9MmlbrnyFCUN3hO7FQVUi9MAsDbDQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "^20.1.1",
|
||||
"@vitest/snapshot": "^1.2.1",
|
||||
"@wdio/config": "8.36.0",
|
||||
"@wdio/globals": "8.36.0",
|
||||
"@wdio/config": "8.36.1",
|
||||
"@wdio/globals": "8.36.1",
|
||||
"@wdio/logger": "8.28.0",
|
||||
"@wdio/protocols": "8.32.0",
|
||||
"@wdio/types": "8.36.0",
|
||||
"@wdio/utils": "8.36.0",
|
||||
"@wdio/types": "8.36.1",
|
||||
"@wdio/utils": "8.36.1",
|
||||
"async-exit-hook": "^2.0.1",
|
||||
"chalk": "^5.2.0",
|
||||
"chokidar": "^3.5.3",
|
||||
@ -1216,7 +1216,7 @@
|
||||
"lodash.union": "^4.6.0",
|
||||
"read-pkg-up": "10.0.0",
|
||||
"recursive-readdir": "^2.2.3",
|
||||
"webdriverio": "8.36.0",
|
||||
"webdriverio": "8.36.1",
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"bin": {
|
||||
@ -1239,14 +1239,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@wdio/config": {
|
||||
"version": "8.36.0",
|
||||
"resolved": "https://registry.npmjs.org/@wdio/config/-/config-8.36.0.tgz",
|
||||
"integrity": "sha512-sAbqnx/G+OsrMquIncFXjM4U0/E0ULMP0jDHZND75r0e1DYYCHmyacrvIHu3Jyxinl9f6+4XQdev6vqdTqPdNg==",
|
||||
"version": "8.36.1",
|
||||
"resolved": "https://registry.npmjs.org/@wdio/config/-/config-8.36.1.tgz",
|
||||
"integrity": "sha512-yCENnym0CrYuLKMJ3fv00WkjCR8QpPqVohGBkq5FvZOZpVJEpoG86Q8l4HtyRnd6ggMTKCA1vTQ/myhbPmZmaQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@wdio/logger": "8.28.0",
|
||||
"@wdio/types": "8.36.0",
|
||||
"@wdio/utils": "8.36.0",
|
||||
"@wdio/types": "8.36.1",
|
||||
"@wdio/utils": "8.36.1",
|
||||
"decamelize": "^6.0.0",
|
||||
"deepmerge-ts": "^5.0.0",
|
||||
"glob": "^10.2.2",
|
||||
@ -1257,29 +1257,29 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@wdio/globals": {
|
||||
"version": "8.36.0",
|
||||
"resolved": "https://registry.npmjs.org/@wdio/globals/-/globals-8.36.0.tgz",
|
||||
"integrity": "sha512-vqMq1hR+iF0lqMNJpk9z+QB9l/QfL1DbvOfNhPtQ13NgctfNg42ffuhEObbzTLQN0MftcnPBu6O3pai79y8bUA==",
|
||||
"version": "8.36.1",
|
||||
"resolved": "https://registry.npmjs.org/@wdio/globals/-/globals-8.36.1.tgz",
|
||||
"integrity": "sha512-Qpj6gZCRNxqdVkTwYyi4JdeYO4tLSUj3Ti6yxO0v9A4IRaKW1tS29KUcGgjL9CFSBKAOi2zRY8vvFz1u6ewxtQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "^16.13 || >=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"expect-webdriverio": "^4.11.2",
|
||||
"webdriverio": "8.36.0"
|
||||
"webdriverio": "8.36.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@wdio/local-runner": {
|
||||
"version": "8.36.0",
|
||||
"resolved": "https://registry.npmjs.org/@wdio/local-runner/-/local-runner-8.36.0.tgz",
|
||||
"integrity": "sha512-MIzbWcXgRQGQQK4H5N39/JFoikOg5cu34l1U6rgw74D6hO79L4RwBg2Oo4TJJYgHUL/4RbVwyeLdb5WDTdluTQ==",
|
||||
"version": "8.36.1",
|
||||
"resolved": "https://registry.npmjs.org/@wdio/local-runner/-/local-runner-8.36.1.tgz",
|
||||
"integrity": "sha512-FYsTzbNGRnrniOsLWrZO7+DLecAS9W75AIzFZQVQxruiDFkGmKY5OV6gsuvMlasaqAQXW1s+w29bqrLY4DxdEw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "^20.1.0",
|
||||
"@wdio/logger": "8.28.0",
|
||||
"@wdio/repl": "8.24.12",
|
||||
"@wdio/runner": "8.36.0",
|
||||
"@wdio/types": "8.36.0",
|
||||
"@wdio/runner": "8.36.1",
|
||||
"@wdio/types": "8.36.1",
|
||||
"async-exit-hook": "^2.0.1",
|
||||
"split2": "^4.1.0",
|
||||
"stream-buffers": "^3.0.2"
|
||||
@ -1316,16 +1316,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@wdio/mocha-framework": {
|
||||
"version": "8.36.0",
|
||||
"resolved": "https://registry.npmjs.org/@wdio/mocha-framework/-/mocha-framework-8.36.0.tgz",
|
||||
"integrity": "sha512-5wZgh1apbSKTtgGwvd//L4kxdaXe30AQ3y9YeJD+OuAJUTYFRjTpMS13bO3pX518imQeB8HCm4aUc2kxs7J81Q==",
|
||||
"version": "8.36.1",
|
||||
"resolved": "https://registry.npmjs.org/@wdio/mocha-framework/-/mocha-framework-8.36.1.tgz",
|
||||
"integrity": "sha512-G0h5AeneMNtoh9CcVQ82OCKj0axxUOEotEcInDu8V6UJbUywNJVL/bdTMKdaq5i84Hnc+s1LUKmLvN95F+lHGA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/mocha": "^10.0.0",
|
||||
"@types/node": "^20.1.0",
|
||||
"@wdio/logger": "8.28.0",
|
||||
"@wdio/types": "8.36.0",
|
||||
"@wdio/utils": "8.36.0",
|
||||
"@wdio/types": "8.36.1",
|
||||
"@wdio/utils": "8.36.1",
|
||||
"mocha": "^10.0.0"
|
||||
},
|
||||
"engines": {
|
||||
@ -1351,14 +1351,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@wdio/reporter": {
|
||||
"version": "8.36.0",
|
||||
"resolved": "https://registry.npmjs.org/@wdio/reporter/-/reporter-8.36.0.tgz",
|
||||
"integrity": "sha512-pkAxqiMC+ljmksOKlK9g6y2NRvrdQiKtxD11rsMwJ6CH4kVDSGIvENw7u3kxg7Qwp0j1rCKf5Hp51npqKQgeDQ==",
|
||||
"version": "8.36.1",
|
||||
"resolved": "https://registry.npmjs.org/@wdio/reporter/-/reporter-8.36.1.tgz",
|
||||
"integrity": "sha512-HcXr9XKq/6kPC9nexMRXIc/ft3Lvp0yCaW5tps01Axus9wbi5ysLHi2z5sB84F2YdpM+aRf7Lac56xkc4Jldeg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "^20.1.0",
|
||||
"@wdio/logger": "8.28.0",
|
||||
"@wdio/types": "8.36.0",
|
||||
"@wdio/types": "8.36.1",
|
||||
"diff": "^5.0.0",
|
||||
"object-inspect": "^1.12.0"
|
||||
},
|
||||
@ -1367,35 +1367,35 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@wdio/runner": {
|
||||
"version": "8.36.0",
|
||||
"resolved": "https://registry.npmjs.org/@wdio/runner/-/runner-8.36.0.tgz",
|
||||
"integrity": "sha512-M2ZDL0gmR2VvVMchi3Pkonva6Gn6eFh6IwVCpT0np7zioaqOksy3IM7Aki8kPKKS88Osip5dAfoKIrY7JpHovA==",
|
||||
"version": "8.36.1",
|
||||
"resolved": "https://registry.npmjs.org/@wdio/runner/-/runner-8.36.1.tgz",
|
||||
"integrity": "sha512-bLkxQ46MLEbzIf30adl2nyz8kxED/V0IjcQASm0VKfNmsG8LOf7iOIz+udOF4GkMoF++5JuONA5abUsyLvwatg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "^20.11.28",
|
||||
"@wdio/config": "8.36.0",
|
||||
"@wdio/globals": "8.36.0",
|
||||
"@wdio/config": "8.36.1",
|
||||
"@wdio/globals": "8.36.1",
|
||||
"@wdio/logger": "8.28.0",
|
||||
"@wdio/types": "8.36.0",
|
||||
"@wdio/utils": "8.36.0",
|
||||
"@wdio/types": "8.36.1",
|
||||
"@wdio/utils": "8.36.1",
|
||||
"deepmerge-ts": "^5.1.0",
|
||||
"expect-webdriverio": "^4.12.0",
|
||||
"gaze": "^1.1.3",
|
||||
"webdriver": "8.36.0",
|
||||
"webdriverio": "8.36.0"
|
||||
"webdriver": "8.36.1",
|
||||
"webdriverio": "8.36.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^16.13 || >=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@wdio/spec-reporter": {
|
||||
"version": "8.36.0",
|
||||
"resolved": "https://registry.npmjs.org/@wdio/spec-reporter/-/spec-reporter-8.36.0.tgz",
|
||||
"integrity": "sha512-GVOiWqVYvzoAo4/4hNVxvyVWVoHyEmAywYhkykyJGL05YpO0oDOZY2kINPePEX5Z+nIsXsiKPmtsGGqWsfQwTw==",
|
||||
"version": "8.36.1",
|
||||
"resolved": "https://registry.npmjs.org/@wdio/spec-reporter/-/spec-reporter-8.36.1.tgz",
|
||||
"integrity": "sha512-VgAd8VQCfwKYz4A3BPDUYNIQxXhRSTaVNbmDzSlYfo5Jekygk7fz0LRFYBpJ69l7eQH0P5nzEyF92oW/rvE3VA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@wdio/reporter": "8.36.0",
|
||||
"@wdio/types": "8.36.0",
|
||||
"@wdio/reporter": "8.36.1",
|
||||
"@wdio/types": "8.36.1",
|
||||
"chalk": "^5.1.2",
|
||||
"easy-table": "^1.2.0",
|
||||
"pretty-ms": "^7.0.0"
|
||||
@ -1417,9 +1417,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@wdio/types": {
|
||||
"version": "8.36.0",
|
||||
"resolved": "https://registry.npmjs.org/@wdio/types/-/types-8.36.0.tgz",
|
||||
"integrity": "sha512-0hw/PaJHqDrbIMvU08w3oMDGg89udSkqWF2hFlGAjOc20quRrhn0F1L+NhFpYdezeRKz5gpgTDIqaQs9RWKq1A==",
|
||||
"version": "8.36.1",
|
||||
"resolved": "https://registry.npmjs.org/@wdio/types/-/types-8.36.1.tgz",
|
||||
"integrity": "sha512-kKtyJbypasKo/VQuJ6dTQQwFtHE9qoygjoCZjrQCLGraRSjOEiqZHPR0497wbeCvcgHIYyImbmcylqZNGUE0CQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "^20.1.0"
|
||||
@ -1429,14 +1429,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@wdio/utils": {
|
||||
"version": "8.36.0",
|
||||
"resolved": "https://registry.npmjs.org/@wdio/utils/-/utils-8.36.0.tgz",
|
||||
"integrity": "sha512-3VAbavN206qkvm6lITtOtTgscFChax7shzqHjUNln+QWMRyELtT81iw32ux2ld+Bg3F60LAmhbGodu0lJH7k2w==",
|
||||
"version": "8.36.1",
|
||||
"resolved": "https://registry.npmjs.org/@wdio/utils/-/utils-8.36.1.tgz",
|
||||
"integrity": "sha512-xmgPHU11/o9n2FeRmDFkPRC0okiwA1i2xOcR2c3aSpuk99XkAm9RaMn/6u9LFaqsCpgaVxazcYEGSceO7U4hZA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@puppeteer/browsers": "^1.6.0",
|
||||
"@wdio/logger": "8.28.0",
|
||||
"@wdio/types": "8.36.0",
|
||||
"@wdio/types": "8.36.1",
|
||||
"decamelize": "^6.0.0",
|
||||
"deepmerge-ts": "^5.1.0",
|
||||
"edgedriver": "^5.3.5",
|
||||
@ -2084,9 +2084,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/chromedriver": {
|
||||
"version": "123.0.4",
|
||||
"resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-123.0.4.tgz",
|
||||
"integrity": "sha512-3Yi7y7q35kkSAOTbRisiww/SL2w+DqafDPAaUShpSuLMmPaOvHQR0i3bm2/33QBiQ8fUb1J/MzppzVL6IDqvhA==",
|
||||
"version": "124.0.1",
|
||||
"resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-124.0.1.tgz",
|
||||
"integrity": "sha512-hxd1tpAUhgMFBZd1h3W7KyMckxofOYCuKAMtcvBDAU0YKKorZcWuq6zP06+Ph0Z1ynPjtgAj0hP9VphCwesjZw==",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@testim/chrome-version": "^1.1.4",
|
||||
@ -8886,18 +8886,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/webdriver": {
|
||||
"version": "8.36.0",
|
||||
"resolved": "https://registry.npmjs.org/webdriver/-/webdriver-8.36.0.tgz",
|
||||
"integrity": "sha512-6fmZI1+OCGbhuGMLBLvA7m9TJvHU1Cyzxqd8rGzIyb8hocR53yh/olfOL1BPcjU1NXmKuU1BePSGF+yiKajiEA==",
|
||||
"version": "8.36.1",
|
||||
"resolved": "https://registry.npmjs.org/webdriver/-/webdriver-8.36.1.tgz",
|
||||
"integrity": "sha512-547RivYCHStVqtiGQBBcABAkzJbPnAWsxpXGzmj5KL+TOM2JF41N2iQRtUxXqr0jme1Nzzye7WS7Y7iSnK6i1g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "^20.1.0",
|
||||
"@types/ws": "^8.5.3",
|
||||
"@wdio/config": "8.36.0",
|
||||
"@wdio/config": "8.36.1",
|
||||
"@wdio/logger": "8.28.0",
|
||||
"@wdio/protocols": "8.32.0",
|
||||
"@wdio/types": "8.36.0",
|
||||
"@wdio/utils": "8.36.0",
|
||||
"@wdio/types": "8.36.1",
|
||||
"@wdio/utils": "8.36.1",
|
||||
"deepmerge-ts": "^5.1.0",
|
||||
"got": "^12.6.1",
|
||||
"ky": "^0.33.0",
|
||||
@ -8908,18 +8908,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/webdriverio": {
|
||||
"version": "8.36.0",
|
||||
"resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-8.36.0.tgz",
|
||||
"integrity": "sha512-4WnEI+OxslHpfSnDXuADaR6bL1M7QxBUEF1mTN56AroOCJelyPvt94yRhszwQnLcJJB2OLn49eUz8M4yBCB51w==",
|
||||
"version": "8.36.1",
|
||||
"resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-8.36.1.tgz",
|
||||
"integrity": "sha512-vzE09oFQeMbOYJ/75jZ13sDIljzC3HH7uoUJKAMAEtyrn/bu1F9Sg/4IDEsvQaRD3pz3ae6SkRld33lcQk6HJA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "^20.1.0",
|
||||
"@wdio/config": "8.36.0",
|
||||
"@wdio/config": "8.36.1",
|
||||
"@wdio/logger": "8.28.0",
|
||||
"@wdio/protocols": "8.32.0",
|
||||
"@wdio/repl": "8.24.12",
|
||||
"@wdio/types": "8.36.0",
|
||||
"@wdio/utils": "8.36.0",
|
||||
"@wdio/types": "8.36.1",
|
||||
"@wdio/utils": "8.36.1",
|
||||
"archiver": "^7.0.0",
|
||||
"aria-query": "^5.0.0",
|
||||
"css-shorthand-properties": "^1.1.1",
|
||||
@ -8936,7 +8936,7 @@
|
||||
"resq": "^1.9.1",
|
||||
"rgb2hex": "0.2.5",
|
||||
"serialize-error": "^11.0.1",
|
||||
"webdriver": "8.36.0"
|
||||
"webdriver": "8.36.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^16.13 || >=18"
|
||||
|
@ -6,10 +6,10 @@
|
||||
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "^7.5.0",
|
||||
"@typescript-eslint/parser": "^7.5.0",
|
||||
"@wdio/cli": "^8.36.0",
|
||||
"@wdio/local-runner": "^8.36.0",
|
||||
"@wdio/mocha-framework": "^8.36.0",
|
||||
"@wdio/spec-reporter": "^8.36.0",
|
||||
"@wdio/cli": "^8.36.1",
|
||||
"@wdio/local-runner": "^8.36.1",
|
||||
"@wdio/mocha-framework": "^8.36.1",
|
||||
"@wdio/spec-reporter": "^8.36.1",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"eslint-plugin-sonarjs": "^0.25.1",
|
||||
@ -32,6 +32,6 @@
|
||||
"node": ">=20"
|
||||
},
|
||||
"dependencies": {
|
||||
"chromedriver": "^123.0.4"
|
||||
"chromedriver": "^124.0.1"
|
||||
}
|
||||
}
|
||||
|
2438
web/package-lock.json
generated
2438
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -38,7 +38,7 @@
|
||||
"@codemirror/theme-one-dark": "^6.1.2",
|
||||
"@formatjs/intl-listformat": "^7.5.5",
|
||||
"@fortawesome/fontawesome-free": "^6.5.2",
|
||||
"@goauthentik/api": "^2024.4.1-1714655911",
|
||||
"@goauthentik/api": "^2024.4.1-1714149838",
|
||||
"@lit-labs/task": "^3.1.0",
|
||||
"@lit/context": "^1.1.1",
|
||||
"@lit/localize": "^0.12.1",
|
||||
@ -46,7 +46,7 @@
|
||||
"@open-wc/lit-helpers": "^0.7.0",
|
||||
"@patternfly/elements": "^3.0.1",
|
||||
"@patternfly/patternfly": "^4.224.2",
|
||||
"@sentry/browser": "^7.111.0",
|
||||
"@sentry/browser": "^7.112.2",
|
||||
"@webcomponents/webcomponentsjs": "^2.8.0",
|
||||
"base64-js": "^1.5.1",
|
||||
"chart.js": "^4.4.2",
|
||||
@ -65,7 +65,7 @@
|
||||
"style-mod": "^4.1.2",
|
||||
"ts-pattern": "^5.1.1",
|
||||
"webcomponent-qr-code": "^1.2.0",
|
||||
"yaml": "^2.4.1"
|
||||
"yaml": "^2.4.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.24.4",
|
||||
@ -81,13 +81,13 @@
|
||||
"@lit/localize-tools": "^0.7.2",
|
||||
"@rollup/plugin-replace": "^5.0.5",
|
||||
"@spotlightjs/spotlight": "^1.2.17",
|
||||
"@storybook/addon-essentials": "^8.0.8",
|
||||
"@storybook/addon-links": "^8.0.8",
|
||||
"@storybook/addon-essentials": "^8.0.9",
|
||||
"@storybook/addon-links": "^8.0.9",
|
||||
"@storybook/api": "^7.6.17",
|
||||
"@storybook/blocks": "^8.0.8",
|
||||
"@storybook/manager-api": "^8.0.8",
|
||||
"@storybook/web-components": "^8.0.8",
|
||||
"@storybook/web-components-vite": "^8.0.8",
|
||||
"@storybook/manager-api": "^8.0.9",
|
||||
"@storybook/web-components": "^8.0.9",
|
||||
"@storybook/web-components-vite": "^8.0.9",
|
||||
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
|
||||
"@types/chart.js": "^2.9.41",
|
||||
"@types/codemirror": "5.60.15",
|
||||
@ -114,10 +114,10 @@
|
||||
"prettier": "^3.2.5",
|
||||
"pseudolocale": "^2.0.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-dom": "^18.3.1",
|
||||
"rollup-plugin-modify": "^3.0.0",
|
||||
"rollup-plugin-postcss-lit": "^2.1.0",
|
||||
"storybook": "^8.0.8",
|
||||
"storybook": "^8.0.9",
|
||||
"storybook-addon-mock": "^5.0.0",
|
||||
"ts-lit-plugin": "^2.0.2",
|
||||
"tslib": "^2.6.2",
|
||||
@ -129,9 +129,9 @@
|
||||
"@esbuild/darwin-arm64": "^0.20.1",
|
||||
"@esbuild/linux-amd64": "^0.18.11",
|
||||
"@esbuild/linux-arm64": "^0.20.1",
|
||||
"@rollup/rollup-darwin-arm64": "4.14.3",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.14.3",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.14.3"
|
||||
"@rollup/rollup-darwin-arm64": "4.17.0",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.17.0",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.17.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
|
@ -29,9 +29,5 @@ export const signatureAlgorithmOptions = toOptions([
|
||||
["RSA-SHA256", SignatureAlgorithmEnum._200104XmldsigMorersaSha256, true],
|
||||
["RSA-SHA384", SignatureAlgorithmEnum._200104XmldsigMorersaSha384],
|
||||
["RSA-SHA512", SignatureAlgorithmEnum._200104XmldsigMorersaSha512],
|
||||
["ECDSA-SHA1", SignatureAlgorithmEnum._200104XmldsigMoreecdsaSha1],
|
||||
["ECDSA-SHA256", SignatureAlgorithmEnum._200104XmldsigMoreecdsaSha256],
|
||||
["ECDSA-SHA384", SignatureAlgorithmEnum._200104XmldsigMoreecdsaSha384],
|
||||
["ECDSA-SHA512", SignatureAlgorithmEnum._200104XmldsigMoreecdsaSha512],
|
||||
["DSA-SHA1", SignatureAlgorithmEnum._200009XmldsigdsaSha1],
|
||||
]);
|
||||
|
@ -6,12 +6,7 @@ import { msg } from "@lit/localize";
|
||||
import { TemplateResult, html } from "lit";
|
||||
import { customElement } from "lit/decorators.js";
|
||||
|
||||
import {
|
||||
AlgEnum,
|
||||
CertificateGenerationRequest,
|
||||
CertificateKeyPair,
|
||||
CryptoApi,
|
||||
} from "@goauthentik/api";
|
||||
import { CertificateGenerationRequest, CertificateKeyPair, CryptoApi } from "@goauthentik/api";
|
||||
|
||||
@customElement("ak-crypto-certificate-generate-form")
|
||||
export class CertificateKeyPairForm extends Form<CertificateGenerationRequest> {
|
||||
@ -45,29 +40,6 @@ export class CertificateKeyPairForm extends Form<CertificateGenerationRequest> {
|
||||
?required=${true}
|
||||
>
|
||||
<input class="pf-c-form-control" type="number" value="365" />
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Private key Algorithm")}
|
||||
?required=${true}
|
||||
name="alg"
|
||||
>
|
||||
<ak-radio
|
||||
.options=${[
|
||||
{
|
||||
label: msg("RSA"),
|
||||
value: AlgEnum.Rsa,
|
||||
default: true,
|
||||
},
|
||||
{
|
||||
label: msg("ECDSA"),
|
||||
value: AlgEnum.Ecdsa,
|
||||
},
|
||||
]}
|
||||
>
|
||||
</ak-radio>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Algorithm used to generate the private key.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal> `;
|
||||
</ak-form-element-horizontal>`;
|
||||
}
|
||||
}
|
||||
|
@ -97,7 +97,7 @@ export class EventListPage extends TablePage<Event> {
|
||||
}
|
||||
|
||||
renderExpanded(item: Event): TemplateResult {
|
||||
return html` <td role="cell" colspan="5">
|
||||
return html` <td role="cell" colspan="3">
|
||||
<div class="pf-c-table__expandable-row-content">
|
||||
<ak-event-info .event=${item as EventWithContext}></ak-event-info>
|
||||
</div>
|
||||
|
@ -12,7 +12,13 @@ import { TemplateResult, html } from "lit";
|
||||
import { customElement } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import { ExpressionPolicy, PoliciesApi } from "@goauthentik/api";
|
||||
import {
|
||||
CoreApi,
|
||||
CoreUsersListRequest,
|
||||
ExpressionPolicy,
|
||||
PoliciesApi,
|
||||
User,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
@customElement("ak-policy-expression-form")
|
||||
export class ExpressionPolicyForm extends BasePolicyForm<ExpressionPolicy> {
|
||||
@ -92,6 +98,39 @@ export class ExpressionPolicyForm extends BasePolicyForm<ExpressionPolicy> {
|
||||
</a>
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal label=${msg("Execution user")} name="executionUser">
|
||||
<ak-search-select
|
||||
.fetchObjects=${async (query?: string): Promise<User[]> => {
|
||||
const args: CoreUsersListRequest = {
|
||||
ordering: "username",
|
||||
};
|
||||
if (query !== undefined) {
|
||||
args.search = query;
|
||||
}
|
||||
const users = await new CoreApi(DEFAULT_CONFIG).coreUsersList(args);
|
||||
return users.results;
|
||||
}}
|
||||
.renderElement=${(user: User): string => {
|
||||
return user.username;
|
||||
}}
|
||||
.renderDescription=${(user: User): TemplateResult => {
|
||||
return html`${user.name}`;
|
||||
}}
|
||||
.value=${(user: User | undefined): number | undefined => {
|
||||
return user?.pk;
|
||||
}}
|
||||
.selected=${(user: User): boolean => {
|
||||
return this.instance?.executionUser === user.pk;
|
||||
}}
|
||||
blankable
|
||||
>
|
||||
</ak-search-select>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"Configure which user the bundled API client authenticates as. When left empty, the API client will inherit the permissions of the user triggering the policy execution.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
</ak-form-group>`;
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ export const SUCCESS_CLASS = "pf-m-success";
|
||||
export const ERROR_CLASS = "pf-m-danger";
|
||||
export const PROGRESS_CLASS = "pf-m-in-progress";
|
||||
export const CURRENT_CLASS = "pf-m-current";
|
||||
export const VERSION = "2024.4.2";
|
||||
export const VERSION = "2024.4.1";
|
||||
export const TITLE_DEFAULT = "authentik";
|
||||
export const ROUTE_SEPARATOR = ";";
|
||||
|
||||
|
@ -44,7 +44,7 @@ export function docLink(path: string): string {
|
||||
const ak = globalAK();
|
||||
// Default case or beta build which should always point to latest
|
||||
if (!ak || ak.build !== "") {
|
||||
return `https://goauthentik.io${path}`;
|
||||
return `https://docs.goauthentik.io${path}`;
|
||||
}
|
||||
return `https://${ak.versionSubdomain}.goauthentik.io${path}`;
|
||||
}
|
||||
|
@ -18,7 +18,6 @@ import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList
|
||||
import PFList from "@patternfly/patternfly/components/List/list.css";
|
||||
import PFTable from "@patternfly/patternfly/components/Table/table.css";
|
||||
import PFFlex from "@patternfly/patternfly/layouts/Flex/flex.css";
|
||||
import PFSplit from "@patternfly/patternfly/layouts/Split/split.css";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
|
||||
import { EventActions, FlowsApi } from "@goauthentik/api";
|
||||
@ -82,7 +81,6 @@ export class EventInfo extends AKElement {
|
||||
PFCard,
|
||||
PFTable,
|
||||
PFList,
|
||||
PFSplit,
|
||||
PFDescriptionList,
|
||||
css`
|
||||
code {
|
||||
@ -248,17 +246,11 @@ export class EventInfo extends AKElement {
|
||||
|
||||
renderModelChanged() {
|
||||
const diff = this.event.context.diff as unknown as {
|
||||
[key: string]: {
|
||||
new_value: unknown;
|
||||
previous_value: unknown;
|
||||
add?: unknown[];
|
||||
remove?: unknown[];
|
||||
clear?: boolean;
|
||||
};
|
||||
[key: string]: { new_value: unknown; previous_value: unknown };
|
||||
};
|
||||
let diffBody = html``;
|
||||
if (diff) {
|
||||
diffBody = html`<div class="pf-l-split__item pf-m-fill">
|
||||
diffBody = html`<div class="pf-l-flex__item">
|
||||
<div class="pf-c-card__title">${msg("Changes made:")}</div>
|
||||
<table class="pf-c-table pf-m-compact pf-m-grid-md" role="grid">
|
||||
<thead>
|
||||
@ -270,36 +262,16 @@ export class EventInfo extends AKElement {
|
||||
</thead>
|
||||
<tbody role="rowgroup">
|
||||
${Object.keys(diff).map((key) => {
|
||||
const value = diff[key];
|
||||
const previousCol = value.previous_value
|
||||
? JSON.stringify(value.previous_value, null, 4)
|
||||
: msg("-");
|
||||
let newCol = html``;
|
||||
if (value.add || value.remove) {
|
||||
newCol = html`<ul class="pf-c-list">
|
||||
${(value.add || value.remove)?.map((item) => {
|
||||
let itemLabel = "";
|
||||
if (value.add) {
|
||||
itemLabel = msg(str`Added ID ${item}`);
|
||||
} else if (value.remove) {
|
||||
itemLabel = msg(str`Removed ID ${item}`);
|
||||
}
|
||||
return html`<li>${itemLabel}</li>`;
|
||||
})}
|
||||
</ul>`;
|
||||
} else if (value.clear) {
|
||||
newCol = html`${msg("Cleared")}`;
|
||||
} else {
|
||||
newCol = html`<pre>
|
||||
${JSON.stringify(value.new_value, null, 4)}</pre
|
||||
>`;
|
||||
}
|
||||
return html` <tr role="row">
|
||||
<td role="cell"><pre>${key}</pre></td>
|
||||
<td role="cell">
|
||||
<pre>${previousCol}</pre>
|
||||
<pre>
|
||||
${JSON.stringify(diff[key].previous_value, null, 4)}</pre
|
||||
>
|
||||
</td>
|
||||
<td role="cell">
|
||||
<pre>${JSON.stringify(diff[key].new_value, null, 4)}</pre>
|
||||
</td>
|
||||
<td role="cell">${newCol}</td>
|
||||
</tr>`;
|
||||
})}
|
||||
</tbody>
|
||||
@ -308,8 +280,8 @@ ${JSON.stringify(value.new_value, null, 4)}</pre
|
||||
</div>`;
|
||||
}
|
||||
return html`
|
||||
<div class="pf-l-split">
|
||||
<div class="pf-l-split__item pf-m-fill">
|
||||
<div class="pf-l-flex">
|
||||
<div class="pf-l-flex__item">
|
||||
<div class="pf-c-card__title">${msg("Affected model:")}</div>
|
||||
<div class="pf-c-card__body">
|
||||
${this.getModelInfo(this.event.context?.model as EventModel)}
|
||||
|
@ -1,23 +1,22 @@
|
||||
import { EVENT_REFRESH } from "@goauthentik/authentik/common/constants";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { authentikBrandContext } from "@goauthentik/elements/AuthentikContexts";
|
||||
import type { ReactiveElementHost } from "@goauthentik/elements/types.js";
|
||||
|
||||
import { ContextProvider } from "@lit/context";
|
||||
import { ReactiveController, ReactiveControllerHost } from "lit";
|
||||
import type { ReactiveController } from "lit";
|
||||
|
||||
import type { CurrentBrand } from "@goauthentik/api";
|
||||
import { CoreApi } from "@goauthentik/api";
|
||||
|
||||
import type { AkInterface } from "./Interface";
|
||||
|
||||
type ReactiveElementHost = Partial<ReactiveControllerHost> & AkInterface;
|
||||
|
||||
export class BrandContextController implements ReactiveController {
|
||||
host!: ReactiveElementHost;
|
||||
host!: ReactiveElementHost<AkInterface>;
|
||||
|
||||
context!: ContextProvider<{ __context__: CurrentBrand | undefined }>;
|
||||
|
||||
constructor(host: ReactiveElementHost) {
|
||||
constructor(host: ReactiveElementHost<AkInterface>) {
|
||||
this.host = host;
|
||||
this.context = new ContextProvider(this.host, {
|
||||
context: authentikBrandContext,
|
||||
|
@ -2,23 +2,22 @@ import { EVENT_REFRESH } from "@goauthentik/authentik/common/constants";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { globalAK } from "@goauthentik/common/global";
|
||||
import { authentikConfigContext } from "@goauthentik/elements/AuthentikContexts";
|
||||
import type { ReactiveElementHost } from "@goauthentik/elements/types.js";
|
||||
|
||||
import { ContextProvider } from "@lit/context";
|
||||
import { ReactiveController, ReactiveControllerHost } from "lit";
|
||||
import type { ReactiveController } from "lit";
|
||||
|
||||
import type { Config } from "@goauthentik/api";
|
||||
import { RootApi } from "@goauthentik/api";
|
||||
|
||||
import type { AkInterface } from "./Interface";
|
||||
|
||||
type ReactiveElementHost = Partial<ReactiveControllerHost> & AkInterface;
|
||||
|
||||
export class ConfigContextController implements ReactiveController {
|
||||
host!: ReactiveElementHost;
|
||||
host!: ReactiveElementHost<AkInterface>;
|
||||
|
||||
context!: ContextProvider<{ __context__: Config | undefined }>;
|
||||
|
||||
constructor(host: ReactiveElementHost) {
|
||||
constructor(host: ReactiveElementHost<AkInterface>) {
|
||||
this.host = host;
|
||||
this.context = new ContextProvider(this.host, {
|
||||
context: authentikConfigContext,
|
||||
|
@ -1,23 +1,22 @@
|
||||
import { EVENT_REFRESH_ENTERPRISE } from "@goauthentik/authentik/common/constants";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { authentikEnterpriseContext } from "@goauthentik/elements/AuthentikContexts";
|
||||
import type { ReactiveElementHost } from "@goauthentik/elements/types.js";
|
||||
|
||||
import { ContextProvider } from "@lit/context";
|
||||
import { ReactiveController, ReactiveControllerHost } from "lit";
|
||||
import type { ReactiveController } from "lit";
|
||||
|
||||
import type { LicenseSummary } from "@goauthentik/api";
|
||||
import { EnterpriseApi } from "@goauthentik/api";
|
||||
|
||||
import type { AkEnterpriseInterface } from "./Interface";
|
||||
|
||||
type ReactiveElementHost = Partial<ReactiveControllerHost> & AkEnterpriseInterface;
|
||||
|
||||
export class EnterpriseContextController implements ReactiveController {
|
||||
host!: ReactiveElementHost;
|
||||
host!: ReactiveElementHost<AkEnterpriseInterface>;
|
||||
|
||||
context!: ContextProvider<{ __context__: LicenseSummary | undefined }>;
|
||||
|
||||
constructor(host: ReactiveElementHost) {
|
||||
constructor(host: ReactiveElementHost<AkEnterpriseInterface>) {
|
||||
this.host = host;
|
||||
this.context = new ContextProvider(this.host, {
|
||||
context: authentikEnterpriseContext,
|
||||
|
@ -1,13 +1,11 @@
|
||||
import { authentikConfigContext } from "@goauthentik/elements/AuthentikContexts";
|
||||
import type { Constructor } from "@goauthentik/elements/types.js";
|
||||
|
||||
import { consume } from "@lit/context";
|
||||
import type { LitElement } from "lit";
|
||||
|
||||
import type { Config } from "@goauthentik/api";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type Constructor<T = object> = new (...args: any[]) => T;
|
||||
|
||||
export function WithAuthentikConfig<T extends Constructor<LitElement>>(
|
||||
superclass: T,
|
||||
subscribe = true,
|
||||
|
@ -1,14 +1,12 @@
|
||||
import { authentikBrandContext } from "@goauthentik/elements/AuthentikContexts";
|
||||
import type { AbstractConstructor } from "@goauthentik/elements/types.js";
|
||||
|
||||
import { consume } from "@lit/context";
|
||||
import type { LitElement } from "lit";
|
||||
|
||||
import type { CurrentBrand } from "@goauthentik/api";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type Constructor<T = object> = abstract new (...args: any[]) => T;
|
||||
|
||||
export function WithBrandConfig<T extends Constructor<LitElement>>(
|
||||
export function WithBrandConfig<T extends AbstractConstructor<LitElement>>(
|
||||
superclass: T,
|
||||
subscribe = true,
|
||||
) {
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { authentikConfigContext } from "@goauthentik/elements/AuthentikContexts";
|
||||
import type { AbstractConstructor } from "@goauthentik/elements/types.js";
|
||||
|
||||
import { consume } from "@lit/context";
|
||||
import type { LitElement } from "lit";
|
||||
@ -6,9 +7,6 @@ import type { LitElement } from "lit";
|
||||
import { CapabilitiesEnum } from "@goauthentik/api";
|
||||
import { Config } from "@goauthentik/api";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type Constructor<T = object> = abstract new (...args: any[]) => T;
|
||||
|
||||
// Using a unique, lexically scoped, and locally static symbol as the field name for the context
|
||||
// means that it's inaccessible to any child class looking for it. It's one of the strongest privacy
|
||||
// guarantees in JavaScript.
|
||||
@ -45,7 +43,7 @@ class WCC {
|
||||
*
|
||||
*/
|
||||
|
||||
export function WithCapabilitiesConfig<T extends Constructor<LitElement>>(
|
||||
export function WithCapabilitiesConfig<T extends AbstractConstructor<LitElement>>(
|
||||
superclass: T,
|
||||
subscribe = true,
|
||||
) {
|
||||
|
@ -1,13 +1,11 @@
|
||||
import { authentikEnterpriseContext } from "@goauthentik/elements/AuthentikContexts";
|
||||
import { Constructor } from "@goauthentik/elements/types.js";
|
||||
|
||||
import { consume } from "@lit/context";
|
||||
import type { LitElement } from "lit";
|
||||
|
||||
import type { LicenseSummary } from "@goauthentik/api";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type Constructor<T = object> = abstract new (...args: any[]) => T;
|
||||
|
||||
export function WithLicenseSummary<T extends Constructor<LitElement>>(
|
||||
superclass: T,
|
||||
subscribe = true,
|
||||
|
11
web/src/elements/types.ts
Normal file
11
web/src/elements/types.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
|
||||
import { ReactiveControllerHost } from "lit";
|
||||
|
||||
export type ReactiveElementHost<T = AKElement> = Partial<ReactiveControllerHost> & T;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type Constructor<T = object> = new (...args: any[]) => T;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type AbstractConstructor<T = object> = abstract new (...args: any[]) => T;
|
@ -8,7 +8,7 @@ import "@goauthentik/elements/EmptyState";
|
||||
import { BaseStage } from "@goauthentik/flow/stages/base";
|
||||
|
||||
import { msg, str } from "@lit/localize";
|
||||
import { CSSResult, PropertyValues, TemplateResult, css, html, nothing } from "lit";
|
||||
import { CSSResult, TemplateResult, css, html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
@ -115,16 +115,14 @@ export class WebAuthnAuthenticatorRegisterStage extends BaseStage<
|
||||
});
|
||||
}
|
||||
|
||||
updated(changedProperties: PropertyValues<this>) {
|
||||
if (changedProperties.has("challenge") && this.challenge !== undefined) {
|
||||
// convert certain members of the PublicKeyCredentialCreateOptions into
|
||||
// byte arrays as expected by the spec.
|
||||
this.publicKeyCredentialCreateOptions = transformCredentialCreateOptions(
|
||||
this.challenge?.registration as PublicKeyCredentialCreationOptions,
|
||||
this.challenge?.registration.user.id,
|
||||
);
|
||||
this.registerWrapper();
|
||||
}
|
||||
firstUpdated(): void {
|
||||
// convert certain members of the PublicKeyCredentialCreateOptions into
|
||||
// byte arrays as expected by the spec.
|
||||
this.publicKeyCredentialCreateOptions = transformCredentialCreateOptions(
|
||||
this.challenge?.registration as PublicKeyCredentialCreationOptions,
|
||||
this.challenge?.registration.user.id,
|
||||
);
|
||||
this.registerWrapper();
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
|
@ -6534,6 +6534,15 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc1673c93148583ba">
|
||||
<source>Request failed. Please try again later.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s85be1f5e7a0fa3b1">
|
||||
<source>Available Roles</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sa59d53ee922c08b5">
|
||||
<source>Selected Roles</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7bfbf84a8ad5883f">
|
||||
<source>Internal Service accounts are created and managed by authentik and cannot be created manually.</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
@ -6803,6 +6803,15 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc1673c93148583ba">
|
||||
<source>Request failed. Please try again later.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s85be1f5e7a0fa3b1">
|
||||
<source>Available Roles</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sa59d53ee922c08b5">
|
||||
<source>Selected Roles</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7bfbf84a8ad5883f">
|
||||
<source>Internal Service accounts are created and managed by authentik and cannot be created manually.</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
@ -6451,6 +6451,15 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc1673c93148583ba">
|
||||
<source>Request failed. Please try again later.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s85be1f5e7a0fa3b1">
|
||||
<source>Available Roles</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sa59d53ee922c08b5">
|
||||
<source>Selected Roles</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7bfbf84a8ad5883f">
|
||||
<source>Internal Service accounts are created and managed by authentik and cannot be created manually.</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
@ -8599,6 +8599,15 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti
|
||||
</trans-unit>
|
||||
<trans-unit id="sc1673c93148583ba">
|
||||
<source>Request failed. Please try again later.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s85be1f5e7a0fa3b1">
|
||||
<source>Available Roles</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sa59d53ee922c08b5">
|
||||
<source>Selected Roles</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7bfbf84a8ad5883f">
|
||||
<source>Internal Service accounts are created and managed by authentik and cannot be created manually.</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
@ -8389,6 +8389,15 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc1673c93148583ba">
|
||||
<source>Request failed. Please try again later.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s85be1f5e7a0fa3b1">
|
||||
<source>Available Roles</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sa59d53ee922c08b5">
|
||||
<source>Selected Roles</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7bfbf84a8ad5883f">
|
||||
<source>Internal Service accounts are created and managed by authentik and cannot be created manually.</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
@ -8233,6 +8233,15 @@ Bindingen naar groepen/gebruikers worden gecontroleerd tegen de gebruiker van de
|
||||
</trans-unit>
|
||||
<trans-unit id="sc1673c93148583ba">
|
||||
<source>Request failed. Please try again later.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s85be1f5e7a0fa3b1">
|
||||
<source>Available Roles</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sa59d53ee922c08b5">
|
||||
<source>Selected Roles</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7bfbf84a8ad5883f">
|
||||
<source>Internal Service accounts are created and managed by authentik and cannot be created manually.</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
@ -6655,6 +6655,15 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc1673c93148583ba">
|
||||
<source>Request failed. Please try again later.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s85be1f5e7a0fa3b1">
|
||||
<source>Available Roles</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sa59d53ee922c08b5">
|
||||
<source>Selected Roles</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7bfbf84a8ad5883f">
|
||||
<source>Internal Service accounts are created and managed by authentik and cannot be created manually.</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
@ -8505,4 +8505,13 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="sc1673c93148583ba">
|
||||
<source>Request failed. Please try again later.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s85be1f5e7a0fa3b1">
|
||||
<source>Available Roles</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sa59d53ee922c08b5">
|
||||
<source>Selected Roles</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7bfbf84a8ad5883f">
|
||||
<source>Internal Service accounts are created and managed by authentik and cannot be created manually.</source>
|
||||
</trans-unit>
|
||||
</body></file></xliff>
|
||||
|
@ -6444,6 +6444,15 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc1673c93148583ba">
|
||||
<source>Request failed. Please try again later.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s85be1f5e7a0fa3b1">
|
||||
<source>Available Roles</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sa59d53ee922c08b5">
|
||||
<source>Selected Roles</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7bfbf84a8ad5883f">
|
||||
<source>Internal Service accounts are created and managed by authentik and cannot be created manually.</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
@ -5362,6 +5362,15 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="sc1673c93148583ba">
|
||||
<source>Request failed. Please try again later.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s85be1f5e7a0fa3b1">
|
||||
<source>Available Roles</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sa59d53ee922c08b5">
|
||||
<source>Selected Roles</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7bfbf84a8ad5883f">
|
||||
<source>Internal Service accounts are created and managed by authentik and cannot be created manually.</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
@ -8601,6 +8601,18 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc1673c93148583ba">
|
||||
<source>Request failed. Please try again later.</source>
|
||||
<target>请求失败。请稍后重试。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s85be1f5e7a0fa3b1">
|
||||
<source>Available Roles</source>
|
||||
<target>可用角色</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sa59d53ee922c08b5">
|
||||
<source>Selected Roles</source>
|
||||
<target>已选角色</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7bfbf84a8ad5883f">
|
||||
<source>Internal Service accounts are created and managed by authentik and cannot be created manually.</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
@ -6492,6 +6492,15 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc1673c93148583ba">
|
||||
<source>Request failed. Please try again later.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s85be1f5e7a0fa3b1">
|
||||
<source>Available Roles</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sa59d53ee922c08b5">
|
||||
<source>Selected Roles</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7bfbf84a8ad5883f">
|
||||
<source>Internal Service accounts are created and managed by authentik and cannot be created manually.</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
@ -8598,6 +8598,18 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="s30d6ff9e15e0a40a">
|
||||
<source>Verifying...</source>
|
||||
<target>正在验证...</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc1673c93148583ba">
|
||||
<source>Request failed. Please try again later.</source>
|
||||
<target>请求失败。请稍后重试。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s85be1f5e7a0fa3b1">
|
||||
<source>Available Roles</source>
|
||||
<target>可用角色</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sa59d53ee922c08b5">
|
||||
<source>Selected Roles</source>
|
||||
<target>已选角色</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
@ -8350,6 +8350,15 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc1673c93148583ba">
|
||||
<source>Request failed. Please try again later.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s85be1f5e7a0fa3b1">
|
||||
<source>Available Roles</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sa59d53ee922c08b5">
|
||||
<source>Selected Roles</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7bfbf84a8ad5883f">
|
||||
<source>Internal Service accounts are created and managed by authentik and cannot be created manually.</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user