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]
|
[bumpversion]
|
||||||
current_version = 2024.4.3
|
current_version = 2024.4.1
|
||||||
tag = True
|
tag = True
|
||||||
commit = 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*))?
|
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"]
|
branch_name = os.environ["GITHUB_REF"]
|
||||||
if os.environ.get("GITHUB_HEAD_REF", "") != "":
|
if os.environ.get("GITHUB_HEAD_REF", "") != "":
|
||||||
branch_name = os.environ["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_names = os.getenv("IMAGE_NAME").split(",")
|
||||||
image_arch = os.getenv("IMAGE_ARCH") or None
|
image_arch = os.getenv("IMAGE_ARCH") or None
|
||||||
@ -54,9 +54,9 @@ image_main_tag = image_tags[0]
|
|||||||
image_tags_rendered = ",".join(image_tags)
|
image_tags_rendered = ",".join(image_tags)
|
||||||
|
|
||||||
with open(os.environ["GITHUB_OUTPUT"], "a+", encoding="utf-8") as _output:
|
with open(os.environ["GITHUB_OUTPUT"], "a+", encoding="utf-8") as _output:
|
||||||
print("shouldBuild=%s" % should_build, file=_output)
|
print(f"shouldBuild={should_build}", file=_output)
|
||||||
print("sha=%s" % sha, file=_output)
|
print(f"sha={sha}", file=_output)
|
||||||
print("version=%s" % version, file=_output)
|
print(f"version={version}", file=_output)
|
||||||
print("prerelease=%s" % prerelease, file=_output)
|
print(f"prerelease={prerelease}", file=_output)
|
||||||
print("imageTags=%s" % image_tags_rendered, file=_output)
|
print(f"imageTags={image_tags_rendered}", file=_output)
|
||||||
print("imageMainTag=%s" % image_main_tag, 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
|
- name: Setup authentik env
|
||||||
uses: ./.github/actions/setup
|
uses: ./.github/actions/setup
|
||||||
- name: Create k8s Kind Cluster
|
- name: Create k8s Kind Cluster
|
||||||
uses: helm/kind-action@v1.9.0
|
uses: helm/kind-action@v1.10.0
|
||||||
- name: run integration
|
- name: run integration
|
||||||
run: |
|
run: |
|
||||||
poetry run coverage run manage.py test tests/integration
|
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
|
- name: Generate API
|
||||||
run: make gen-client-go
|
run: make gen-client-go
|
||||||
- name: golangci-lint
|
- name: golangci-lint
|
||||||
uses: golangci/golangci-lint-action@v4
|
uses: golangci/golangci-lint-action@v5
|
||||||
with:
|
with:
|
||||||
version: v1.54.2
|
version: v1.54.2
|
||||||
args: --timeout 5000s --verbose
|
args: --timeout 5000s --verbose
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from os import environ
|
from os import environ
|
||||||
|
|
||||||
__version__ = "2024.4.3"
|
__version__ = "2024.4.1"
|
||||||
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -73,6 +73,11 @@ def auth_user_lookup(raw_header: bytes) -> User | None:
|
|||||||
if user:
|
if user:
|
||||||
CTX_AUTH_VIA.set("secret_key")
|
CTX_AUTH_VIA.set("secret_key")
|
||||||
return user
|
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")
|
raise AuthenticationFailed("Token invalid/expired")
|
||||||
|
|
||||||
|
|
||||||
@ -90,6 +95,13 @@ def token_secret_key(value: str) -> User | None:
|
|||||||
return outpost.user
|
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):
|
class TokenAuthentication(BaseAuthentication):
|
||||||
"""Token-based authentication using HTTP Bearer authentication"""
|
"""Token-based authentication using HTTP Bearer authentication"""
|
||||||
|
|
||||||
|
|||||||
@ -45,13 +45,6 @@ class TokenSerializer(ManagedSerializer, ModelSerializer):
|
|||||||
if SERIALIZER_CONTEXT_BLUEPRINT in self.context:
|
if SERIALIZER_CONTEXT_BLUEPRINT in self.context:
|
||||||
self.fields["key"] = CharField(required=False)
|
self.fields["key"] = CharField(required=False)
|
||||||
|
|
||||||
def validate_user(self, user: User):
|
|
||||||
"""Ensure user of token cannot be changed"""
|
|
||||||
if self.instance and self.instance.user_id:
|
|
||||||
if user.pk != self.instance.user_id:
|
|
||||||
raise ValidationError("User cannot be changed")
|
|
||||||
return user
|
|
||||||
|
|
||||||
def validate(self, attrs: dict[Any, str]) -> dict[Any, str]:
|
def validate(self, attrs: dict[Any, str]) -> dict[Any, str]:
|
||||||
"""Ensure only API or App password tokens are created."""
|
"""Ensure only API or App password tokens are created."""
|
||||||
request: Request = self.context.get("request")
|
request: Request = self.context.get("request")
|
||||||
|
|||||||
@ -36,7 +36,7 @@ class PropertyMappingEvaluator(BaseEvaluator):
|
|||||||
_filename = model.name
|
_filename = model.name
|
||||||
else:
|
else:
|
||||||
_filename = str(model)
|
_filename = str(model)
|
||||||
super().__init__(filename=_filename)
|
super().__init__(None, filename=_filename)
|
||||||
req = PolicyRequest(user=User())
|
req = PolicyRequest(user=User())
|
||||||
req.obj = model
|
req.obj = model
|
||||||
if user:
|
if user:
|
||||||
|
|||||||
@ -13,7 +13,7 @@ from django.utils.translation import gettext as _
|
|||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
from authentik.core.models import Source, SourceUserMatchingModes, User, UserSourceConnection
|
from authentik.core.models import Source, SourceUserMatchingModes, User, UserSourceConnection
|
||||||
from authentik.core.sources.stage import PLAN_CONTEXT_SOURCES_CONNECTION, PostSourceStage
|
from authentik.core.sources.stage import PLAN_CONTEXT_SOURCES_CONNECTION, PostUserEnrollmentStage
|
||||||
from authentik.events.models import Event, EventAction
|
from authentik.events.models import Event, EventAction
|
||||||
from authentik.flows.exceptions import FlowNonApplicableException
|
from authentik.flows.exceptions import FlowNonApplicableException
|
||||||
from authentik.flows.models import Flow, FlowToken, Stage, in_memory_stage
|
from authentik.flows.models import Flow, FlowToken, Stage, in_memory_stage
|
||||||
@ -100,6 +100,8 @@ class SourceFlowManager:
|
|||||||
if self.request.user.is_authenticated:
|
if self.request.user.is_authenticated:
|
||||||
new_connection.user = self.request.user
|
new_connection.user = self.request.user
|
||||||
new_connection = self.update_connection(new_connection, **kwargs)
|
new_connection = self.update_connection(new_connection, **kwargs)
|
||||||
|
|
||||||
|
new_connection.save()
|
||||||
return Action.LINK, new_connection
|
return Action.LINK, new_connection
|
||||||
|
|
||||||
existing_connections = self.connection_type.objects.filter(
|
existing_connections = self.connection_type.objects.filter(
|
||||||
@ -146,6 +148,7 @@ class SourceFlowManager:
|
|||||||
]:
|
]:
|
||||||
new_connection.user = user
|
new_connection.user = user
|
||||||
new_connection = self.update_connection(new_connection, **kwargs)
|
new_connection = self.update_connection(new_connection, **kwargs)
|
||||||
|
new_connection.save()
|
||||||
return Action.LINK, new_connection
|
return Action.LINK, new_connection
|
||||||
if self.source.user_matching_mode in [
|
if self.source.user_matching_mode in [
|
||||||
SourceUserMatchingModes.EMAIL_DENY,
|
SourceUserMatchingModes.EMAIL_DENY,
|
||||||
@ -206,9 +209,13 @@ class SourceFlowManager:
|
|||||||
|
|
||||||
def get_stages_to_append(self, flow: Flow) -> list[Stage]:
|
def get_stages_to_append(self, flow: Flow) -> list[Stage]:
|
||||||
"""Hook to override stages which are appended to the flow"""
|
"""Hook to override stages which are appended to the flow"""
|
||||||
return [
|
if not self.source.enrollment_flow:
|
||||||
in_memory_stage(PostSourceStage),
|
return []
|
||||||
]
|
if flow.slug == self.source.enrollment_flow.slug:
|
||||||
|
return [
|
||||||
|
in_memory_stage(PostUserEnrollmentStage),
|
||||||
|
]
|
||||||
|
return []
|
||||||
|
|
||||||
def _prepare_flow(
|
def _prepare_flow(
|
||||||
self,
|
self,
|
||||||
@ -262,9 +269,6 @@ class SourceFlowManager:
|
|||||||
)
|
)
|
||||||
# We run the Flow planner here so we can pass the Pending user in the context
|
# We run the Flow planner here so we can pass the Pending user in the context
|
||||||
planner = FlowPlanner(flow)
|
planner = FlowPlanner(flow)
|
||||||
# We append some stages so the initial flow we get might be empty
|
|
||||||
planner.allow_empty_flows = True
|
|
||||||
planner.use_cache = False
|
|
||||||
plan = planner.plan(self.request, kwargs)
|
plan = planner.plan(self.request, kwargs)
|
||||||
for stage in self.get_stages_to_append(flow):
|
for stage in self.get_stages_to_append(flow):
|
||||||
plan.append_stage(stage)
|
plan.append_stage(stage)
|
||||||
@ -323,7 +327,7 @@ class SourceFlowManager:
|
|||||||
reverse(
|
reverse(
|
||||||
"authentik_core:if-user",
|
"authentik_core:if-user",
|
||||||
)
|
)
|
||||||
+ "#/settings;page-sources"
|
+ f"#/settings;page-{self.source.slug}"
|
||||||
)
|
)
|
||||||
|
|
||||||
def handle_enroll(
|
def handle_enroll(
|
||||||
|
|||||||
@ -10,7 +10,7 @@ from authentik.flows.stage import StageView
|
|||||||
PLAN_CONTEXT_SOURCES_CONNECTION = "goauthentik.io/sources/connection"
|
PLAN_CONTEXT_SOURCES_CONNECTION = "goauthentik.io/sources/connection"
|
||||||
|
|
||||||
|
|
||||||
class PostSourceStage(StageView):
|
class PostUserEnrollmentStage(StageView):
|
||||||
"""Dynamically injected stage which saves the Connection after
|
"""Dynamically injected stage which saves the Connection after
|
||||||
the user has been enrolled."""
|
the user has been enrolled."""
|
||||||
|
|
||||||
@ -21,12 +21,10 @@ class PostSourceStage(StageView):
|
|||||||
]
|
]
|
||||||
user: User = self.executor.plan.context[PLAN_CONTEXT_PENDING_USER]
|
user: User = self.executor.plan.context[PLAN_CONTEXT_PENDING_USER]
|
||||||
connection.user = user
|
connection.user = user
|
||||||
linked = connection.pk is None
|
|
||||||
connection.save()
|
connection.save()
|
||||||
if linked:
|
Event.new(
|
||||||
Event.new(
|
EventAction.SOURCE_LINKED,
|
||||||
EventAction.SOURCE_LINKED,
|
message="Linked Source",
|
||||||
message="Linked Source",
|
source=connection.source,
|
||||||
source=connection.source,
|
).from_http(self.request)
|
||||||
).from_http(self.request)
|
|
||||||
return self.executor.stage_ok()
|
return self.executor.stage_ok()
|
||||||
|
|||||||
@ -2,9 +2,7 @@
|
|||||||
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from django.conf import ImproperlyConfigured
|
|
||||||
from django.contrib.sessions.backends.cache import KEY_PREFIX
|
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.core.cache import cache
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
@ -17,7 +15,6 @@ from authentik.core.models import (
|
|||||||
User,
|
User,
|
||||||
)
|
)
|
||||||
from authentik.events.system_tasks import SystemTask, TaskStatus, prefill_task
|
from authentik.events.system_tasks import SystemTask, TaskStatus, prefill_task
|
||||||
from authentik.lib.config import CONFIG
|
|
||||||
from authentik.root.celery import CELERY_APP
|
from authentik.root.celery import CELERY_APP
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
@ -42,31 +39,16 @@ def clean_expired_models(self: SystemTask):
|
|||||||
amount = 0
|
amount = 0
|
||||||
|
|
||||||
for session in AuthenticatedSession.objects.all():
|
for session in AuthenticatedSession.objects.all():
|
||||||
match CONFIG.get("session_storage", "cache"):
|
cache_key = f"{KEY_PREFIX}{session.session_key}"
|
||||||
case "cache":
|
value = None
|
||||||
cache_key = f"{KEY_PREFIX}{session.session_key}"
|
try:
|
||||||
value = None
|
value = cache.get(cache_key)
|
||||||
try:
|
|
||||||
value = cache.get(cache_key)
|
|
||||||
|
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
LOGGER.debug("Failed to get session from cache", exc=exc)
|
LOGGER.debug("Failed to get session from cache", exc=exc)
|
||||||
if not value:
|
if not value:
|
||||||
session.delete()
|
session.delete()
|
||||||
amount += 1
|
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"
|
|
||||||
)
|
|
||||||
LOGGER.debug("Expired sessions", model=AuthenticatedSession, amount=amount)
|
LOGGER.debug("Expired sessions", model=AuthenticatedSession, amount=amount)
|
||||||
|
|
||||||
messages.append(f"Expired {amount} {AuthenticatedSession._meta.verbose_name_plural}")
|
messages.append(f"Expired {amount} {AuthenticatedSession._meta.verbose_name_plural}")
|
||||||
|
|||||||
@ -66,14 +66,11 @@ class TestPropertyMappings(TestCase):
|
|||||||
expression="return request.http_request.path",
|
expression="return request.http_request.path",
|
||||||
)
|
)
|
||||||
http_request = self.factory.get("/")
|
http_request = self.factory.get("/")
|
||||||
tmpl = (
|
tmpl = f"""
|
||||||
"""
|
res = ak_call_policy('{expr.name}')
|
||||||
res = ak_call_policy('%s')
|
|
||||||
result = [request.http_request.path, res.raw_result]
|
result = [request.http_request.path, res.raw_result]
|
||||||
return result
|
return result
|
||||||
"""
|
"""
|
||||||
% expr.name
|
|
||||||
)
|
|
||||||
evaluator = PropertyMapping(expression=tmpl, name=generate_id())
|
evaluator = PropertyMapping(expression=tmpl, name=generate_id())
|
||||||
res = evaluator.evaluate(self.user, http_request)
|
res = evaluator.evaluate(self.user, http_request)
|
||||||
self.assertEqual(res, ["/", "/"])
|
self.assertEqual(res, ["/", "/"])
|
||||||
|
|||||||
@ -2,15 +2,11 @@
|
|||||||
|
|
||||||
from django.contrib.auth.models import AnonymousUser
|
from django.contrib.auth.models import AnonymousUser
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.urls import reverse
|
|
||||||
from guardian.utils import get_anonymous_user
|
from guardian.utils import get_anonymous_user
|
||||||
|
|
||||||
from authentik.core.models import SourceUserMatchingModes, User
|
from authentik.core.models import SourceUserMatchingModes, User
|
||||||
from authentik.core.sources.flow_manager import Action
|
from authentik.core.sources.flow_manager import Action
|
||||||
from authentik.core.sources.stage import PostSourceStage
|
|
||||||
from authentik.core.tests.utils import create_test_flow
|
from authentik.core.tests.utils import create_test_flow
|
||||||
from authentik.flows.planner import FlowPlan
|
|
||||||
from authentik.flows.views.executor import SESSION_KEY_PLAN
|
|
||||||
from authentik.lib.generators import generate_id
|
from authentik.lib.generators import generate_id
|
||||||
from authentik.lib.tests.utils import get_request
|
from authentik.lib.tests.utils import get_request
|
||||||
from authentik.policies.denied import AccessDeniedResponse
|
from authentik.policies.denied import AccessDeniedResponse
|
||||||
@ -25,62 +21,42 @@ class TestSourceFlowManager(TestCase):
|
|||||||
|
|
||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.authentication_flow = create_test_flow()
|
self.source: OAuthSource = OAuthSource.objects.create(name="test")
|
||||||
self.enrollment_flow = create_test_flow()
|
|
||||||
self.source: OAuthSource = OAuthSource.objects.create(
|
|
||||||
name=generate_id(),
|
|
||||||
slug=generate_id(),
|
|
||||||
authentication_flow=self.authentication_flow,
|
|
||||||
enrollment_flow=self.enrollment_flow,
|
|
||||||
)
|
|
||||||
self.identifier = generate_id()
|
self.identifier = generate_id()
|
||||||
|
|
||||||
def test_unauthenticated_enroll(self):
|
def test_unauthenticated_enroll(self):
|
||||||
"""Test un-authenticated user enrolling"""
|
"""Test un-authenticated user enrolling"""
|
||||||
request = get_request("/", user=AnonymousUser())
|
flow_manager = OAuthSourceFlowManager(
|
||||||
flow_manager = OAuthSourceFlowManager(self.source, request, self.identifier, {})
|
self.source, get_request("/", user=AnonymousUser()), self.identifier, {}
|
||||||
|
)
|
||||||
action, _ = flow_manager.get_action()
|
action, _ = flow_manager.get_action()
|
||||||
self.assertEqual(action, Action.ENROLL)
|
self.assertEqual(action, Action.ENROLL)
|
||||||
response = flow_manager.get_flow()
|
flow_manager.get_flow()
|
||||||
self.assertEqual(response.status_code, 302)
|
|
||||||
flow_plan: FlowPlan = request.session[SESSION_KEY_PLAN]
|
|
||||||
self.assertEqual(flow_plan.bindings[0].stage.view, PostSourceStage)
|
|
||||||
|
|
||||||
def test_unauthenticated_auth(self):
|
def test_unauthenticated_auth(self):
|
||||||
"""Test un-authenticated user authenticating"""
|
"""Test un-authenticated user authenticating"""
|
||||||
UserOAuthSourceConnection.objects.create(
|
UserOAuthSourceConnection.objects.create(
|
||||||
user=get_anonymous_user(), source=self.source, identifier=self.identifier
|
user=get_anonymous_user(), source=self.source, identifier=self.identifier
|
||||||
)
|
)
|
||||||
request = get_request("/", user=AnonymousUser())
|
|
||||||
flow_manager = OAuthSourceFlowManager(self.source, request, self.identifier, {})
|
flow_manager = OAuthSourceFlowManager(
|
||||||
|
self.source, get_request("/", user=AnonymousUser()), self.identifier, {}
|
||||||
|
)
|
||||||
action, _ = flow_manager.get_action()
|
action, _ = flow_manager.get_action()
|
||||||
self.assertEqual(action, Action.AUTH)
|
self.assertEqual(action, Action.AUTH)
|
||||||
response = flow_manager.get_flow()
|
flow_manager.get_flow()
|
||||||
self.assertEqual(response.status_code, 302)
|
|
||||||
flow_plan: FlowPlan = request.session[SESSION_KEY_PLAN]
|
|
||||||
self.assertEqual(flow_plan.bindings[0].stage.view, PostSourceStage)
|
|
||||||
|
|
||||||
def test_authenticated_link(self):
|
def test_authenticated_link(self):
|
||||||
"""Test authenticated user linking"""
|
"""Test authenticated user linking"""
|
||||||
user = User.objects.create(username="foo", email="foo@bar.baz")
|
UserOAuthSourceConnection.objects.create(
|
||||||
request = get_request("/", user=user)
|
user=get_anonymous_user(), source=self.source, identifier=self.identifier
|
||||||
flow_manager = OAuthSourceFlowManager(self.source, request, self.identifier, {})
|
|
||||||
action, connection = flow_manager.get_action()
|
|
||||||
self.assertEqual(action, Action.LINK)
|
|
||||||
self.assertIsNone(connection.pk)
|
|
||||||
response = flow_manager.get_flow()
|
|
||||||
self.assertEqual(response.status_code, 302)
|
|
||||||
self.assertEqual(
|
|
||||||
response.url,
|
|
||||||
reverse("authentik_core:if-user") + "#/settings;page-sources",
|
|
||||||
)
|
)
|
||||||
|
user = User.objects.create(username="foo", email="foo@bar.baz")
|
||||||
def test_unauthenticated_link(self):
|
flow_manager = OAuthSourceFlowManager(
|
||||||
"""Test un-authenticated user linking"""
|
self.source, get_request("/", user=user), self.identifier, {}
|
||||||
flow_manager = OAuthSourceFlowManager(self.source, get_request("/"), self.identifier, {})
|
)
|
||||||
action, connection = flow_manager.get_action()
|
action, _ = flow_manager.get_action()
|
||||||
self.assertEqual(action, Action.LINK)
|
self.assertEqual(action, Action.LINK)
|
||||||
self.assertIsNone(connection.pk)
|
|
||||||
flow_manager.get_flow()
|
flow_manager.get_flow()
|
||||||
|
|
||||||
def test_unauthenticated_enroll_email(self):
|
def test_unauthenticated_enroll_email(self):
|
||||||
|
|||||||
@ -13,8 +13,9 @@ from authentik.core.models import (
|
|||||||
USER_ATTRIBUTE_TOKEN_MAXIMUM_LIFETIME,
|
USER_ATTRIBUTE_TOKEN_MAXIMUM_LIFETIME,
|
||||||
Token,
|
Token,
|
||||||
TokenIntents,
|
TokenIntents,
|
||||||
|
User,
|
||||||
)
|
)
|
||||||
from authentik.core.tests.utils import create_test_admin_user, create_test_user
|
from authentik.core.tests.utils import create_test_admin_user
|
||||||
from authentik.lib.generators import generate_id
|
from authentik.lib.generators import generate_id
|
||||||
|
|
||||||
|
|
||||||
@ -23,7 +24,7 @@ class TestTokenAPI(APITestCase):
|
|||||||
|
|
||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.user = create_test_user()
|
self.user = User.objects.create(username="testuser")
|
||||||
self.admin = create_test_admin_user()
|
self.admin = create_test_admin_user()
|
||||||
self.client.force_login(self.user)
|
self.client.force_login(self.user)
|
||||||
|
|
||||||
@ -153,24 +154,6 @@ class TestTokenAPI(APITestCase):
|
|||||||
self.assertEqual(token.expiring, True)
|
self.assertEqual(token.expiring, True)
|
||||||
self.assertNotEqual(token.expires.timestamp(), expires.timestamp())
|
self.assertNotEqual(token.expires.timestamp(), expires.timestamp())
|
||||||
|
|
||||||
def test_token_change_user(self):
|
|
||||||
"""Test creating a token and then changing the user"""
|
|
||||||
ident = generate_id()
|
|
||||||
response = self.client.post(reverse("authentik_api:token-list"), {"identifier": ident})
|
|
||||||
self.assertEqual(response.status_code, 201)
|
|
||||||
token = Token.objects.get(identifier=ident)
|
|
||||||
self.assertEqual(token.user, self.user)
|
|
||||||
self.assertEqual(token.intent, TokenIntents.INTENT_API)
|
|
||||||
self.assertEqual(token.expiring, True)
|
|
||||||
self.assertTrue(self.user.has_perm("authentik_core.view_token_key", token))
|
|
||||||
response = self.client.put(
|
|
||||||
reverse("authentik_api:token-detail", kwargs={"identifier": ident}),
|
|
||||||
data={"identifier": "user_token_poc_v3", "intent": "api", "user": self.admin.pk},
|
|
||||||
)
|
|
||||||
self.assertEqual(response.status_code, 400)
|
|
||||||
token.refresh_from_db()
|
|
||||||
self.assertEqual(token.user, self.user)
|
|
||||||
|
|
||||||
def test_list(self):
|
def test_list(self):
|
||||||
"""Test Token List (Test normal authentication)"""
|
"""Test Token List (Test normal authentication)"""
|
||||||
Token.objects.all().delete()
|
Token.objects.all().delete()
|
||||||
|
|||||||
@ -4,7 +4,7 @@ from django.utils.text import slugify
|
|||||||
|
|
||||||
from authentik.brands.models import Brand
|
from authentik.brands.models import Brand
|
||||||
from authentik.core.models import Group, User
|
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.crypto.models import CertificateKeyPair
|
||||||
from authentik.flows.models import Flow, FlowDesignation
|
from authentik.flows.models import Flow, FlowDesignation
|
||||||
from authentik.lib.generators import generate_id
|
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)
|
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"""
|
"""Generate a certificate for testing"""
|
||||||
builder = CertificateBuilder(f"{generate_id()}.self-signed.goauthentik.io")
|
builder = CertificateBuilder(
|
||||||
builder.alg = alg
|
name=f"{generate_id()}.self-signed.goauthentik.io",
|
||||||
|
use_ec_private_key=use_ec_private_key,
|
||||||
|
)
|
||||||
builder.build(
|
builder.build(
|
||||||
subject_alt_names=[f"{generate_id()}.self-signed.goauthentik.io"],
|
subject_alt_names=[f"{generate_id()}.self-signed.goauthentik.io"],
|
||||||
validity_days=360,
|
validity_days=360,
|
||||||
|
|||||||
@ -14,13 +14,7 @@ from drf_spectacular.types import OpenApiTypes
|
|||||||
from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema
|
from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.exceptions import ValidationError
|
from rest_framework.exceptions import ValidationError
|
||||||
from rest_framework.fields import (
|
from rest_framework.fields import CharField, DateTimeField, IntegerField, SerializerMethodField
|
||||||
CharField,
|
|
||||||
ChoiceField,
|
|
||||||
DateTimeField,
|
|
||||||
IntegerField,
|
|
||||||
SerializerMethodField,
|
|
||||||
)
|
|
||||||
from rest_framework.filters import OrderingFilter, SearchFilter
|
from rest_framework.filters import OrderingFilter, SearchFilter
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
from rest_framework.response import Response
|
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.used_by import UsedByMixin
|
||||||
from authentik.core.api.utils import PassiveSerializer
|
from authentik.core.api.utils import PassiveSerializer
|
||||||
from authentik.crypto.apps import MANAGED_KEY
|
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.crypto.models import CertificateKeyPair
|
||||||
from authentik.events.models import Event, EventAction
|
from authentik.events.models import Event, EventAction
|
||||||
from authentik.rbac.decorators import permission_required
|
from authentik.rbac.decorators import permission_required
|
||||||
@ -184,7 +178,6 @@ class CertificateGenerationSerializer(PassiveSerializer):
|
|||||||
common_name = CharField()
|
common_name = CharField()
|
||||||
subject_alt_name = CharField(required=False, allow_blank=True, label=_("Subject-alt name"))
|
subject_alt_name = CharField(required=False, allow_blank=True, label=_("Subject-alt name"))
|
||||||
validity_days = IntegerField(initial=365)
|
validity_days = IntegerField(initial=365)
|
||||||
alg = ChoiceField(default=PrivateKeyAlg.RSA, choices=PrivateKeyAlg.choices)
|
|
||||||
|
|
||||||
|
|
||||||
class CertificateKeyPairFilter(FilterSet):
|
class CertificateKeyPairFilter(FilterSet):
|
||||||
@ -247,7 +240,6 @@ class CertificateKeyPairViewSet(UsedByMixin, ModelViewSet):
|
|||||||
raw_san = data.validated_data.get("subject_alt_name", "")
|
raw_san = data.validated_data.get("subject_alt_name", "")
|
||||||
sans = raw_san.split(",") if raw_san != "" else []
|
sans = raw_san.split(",") if raw_san != "" else []
|
||||||
builder = CertificateBuilder(data.validated_data["common_name"])
|
builder = CertificateBuilder(data.validated_data["common_name"])
|
||||||
builder.alg = data.validated_data["alg"]
|
|
||||||
builder.build(
|
builder.build(
|
||||||
subject_alt_names=sans,
|
subject_alt_names=sans,
|
||||||
validity_days=int(data.validated_data["validity_days"]),
|
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 import ec, rsa
|
||||||
from cryptography.hazmat.primitives.asymmetric.types import PrivateKeyTypes
|
from cryptography.hazmat.primitives.asymmetric.types import PrivateKeyTypes
|
||||||
from cryptography.x509.oid import NameOID
|
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 import __version__
|
||||||
from authentik.crypto.models import CertificateKeyPair
|
from authentik.crypto.models import CertificateKeyPair
|
||||||
|
|
||||||
|
|
||||||
class PrivateKeyAlg(models.TextChoices):
|
|
||||||
"""Algorithm to create private key with"""
|
|
||||||
|
|
||||||
RSA = "rsa", _("rsa")
|
|
||||||
ECDSA = "ecdsa", _("ecdsa")
|
|
||||||
|
|
||||||
|
|
||||||
class CertificateBuilder:
|
class CertificateBuilder:
|
||||||
"""Build self-signed certificates"""
|
"""Build self-signed certificates"""
|
||||||
|
|
||||||
common_name: str
|
common_name: str
|
||||||
alg: PrivateKeyAlg
|
|
||||||
|
|
||||||
def __init__(self, name: str):
|
_use_ec_private_key: bool
|
||||||
self.alg = PrivateKeyAlg.RSA
|
|
||||||
|
def __init__(self, name: str, use_ec_private_key=False):
|
||||||
|
self._use_ec_private_key = use_ec_private_key
|
||||||
self.__public_key = None
|
self.__public_key = None
|
||||||
self.__private_key = None
|
self.__private_key = None
|
||||||
self.__builder = None
|
self.__builder = None
|
||||||
@ -50,13 +42,11 @@ class CertificateBuilder:
|
|||||||
|
|
||||||
def generate_private_key(self) -> PrivateKeyTypes:
|
def generate_private_key(self) -> PrivateKeyTypes:
|
||||||
"""Generate private key"""
|
"""Generate private key"""
|
||||||
if self.alg == PrivateKeyAlg.ECDSA:
|
if self._use_ec_private_key:
|
||||||
return ec.generate_private_key(curve=ec.SECP256R1())
|
return ec.generate_private_key(curve=ec.SECP256R1())
|
||||||
if self.alg == PrivateKeyAlg.RSA:
|
return rsa.generate_private_key(
|
||||||
return rsa.generate_private_key(
|
public_exponent=65537, key_size=4096, backend=default_backend()
|
||||||
public_exponent=65537, key_size=4096, backend=default_backend()
|
)
|
||||||
)
|
|
||||||
raise ValueError(f"Invalid alg: {self.alg}")
|
|
||||||
|
|
||||||
def build(
|
def build(
|
||||||
self,
|
self,
|
||||||
|
|||||||
@ -2,12 +2,11 @@
|
|||||||
|
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from django.apps.registry import apps
|
from django.apps.registry import apps
|
||||||
from django.core.files import File
|
from django.core.files import File
|
||||||
from django.db import connection
|
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.expressions import BaseExpression, Combinable
|
||||||
from django.db.models.signals import post_init
|
from django.db.models.signals import post_init
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
@ -45,7 +44,7 @@ class EnterpriseAuditMiddleware(AuditMiddleware):
|
|||||||
post_init.disconnect(dispatch_uid=request.request_id)
|
post_init.disconnect(dispatch_uid=request.request_id)
|
||||||
|
|
||||||
def serialize_simple(self, model: Model) -> dict:
|
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"""
|
resolved"""
|
||||||
data = {}
|
data = {}
|
||||||
deferred_fields = model.get_deferred_fields()
|
deferred_fields = model.get_deferred_fields()
|
||||||
@ -71,9 +70,6 @@ class EnterpriseAuditMiddleware(AuditMiddleware):
|
|||||||
for key, value in before.items():
|
for key, value in before.items():
|
||||||
if after.get(key) != value:
|
if after.get(key) != value:
|
||||||
diff[key] = {"previous_value": value, "new_value": after.get(key)}
|
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)
|
return sanitize_item(diff)
|
||||||
|
|
||||||
def post_init_handler(self, request: HttpRequest, sender, instance: Model, **_):
|
def post_init_handler(self, request: HttpRequest, sender, instance: Model, **_):
|
||||||
@ -102,37 +98,8 @@ class EnterpriseAuditMiddleware(AuditMiddleware):
|
|||||||
thread_kwargs = {}
|
thread_kwargs = {}
|
||||||
if hasattr(instance, "_previous_state") or created:
|
if hasattr(instance, "_previous_state") or created:
|
||||||
prev_state = getattr(instance, "_previous_state", {})
|
prev_state = getattr(instance, "_previous_state", {})
|
||||||
if created:
|
|
||||||
prev_state = {}
|
|
||||||
# Get current state
|
# Get current state
|
||||||
new_state = self.serialize_simple(instance)
|
new_state = self.serialize_simple(instance)
|
||||||
diff = self.diff(prev_state, new_state)
|
diff = self.diff(prev_state, new_state)
|
||||||
thread_kwargs["diff"] = diff
|
thread_kwargs["diff"] = diff
|
||||||
return super().post_save_handler(request, sender, instance, created, thread_kwargs, **_)
|
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.apps import apps
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.urls import reverse
|
from django.test import TestCase
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
class TestEnterpriseAudit(APITestCase):
|
class TestEnterpriseAudit(TestCase):
|
||||||
"""Test audit middleware"""
|
|
||||||
|
|
||||||
def setUp(self) -> None:
|
|
||||||
self.user = create_test_admin_user()
|
|
||||||
|
|
||||||
def test_import(self):
|
def test_import(self):
|
||||||
"""Ensure middleware is imported when app.ready is called"""
|
"""Ensure middleware is imported when app.ready is called"""
|
||||||
@ -29,182 +16,3 @@ class TestEnterpriseAudit(APITestCase):
|
|||||||
self.assertIn(
|
self.assertIn(
|
||||||
"authentik.enterprise.audit.middleware.EnterpriseAuditMiddleware", settings.MIDDLEWARE
|
"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),
|
model=model_to_dict(instance),
|
||||||
).run()
|
).run()
|
||||||
|
|
||||||
def m2m_changed_handler(
|
def m2m_changed_handler(self, request: HttpRequest, sender, instance: Model, action: str, **_):
|
||||||
self,
|
|
||||||
request: HttpRequest,
|
|
||||||
sender,
|
|
||||||
instance: Model,
|
|
||||||
action: str,
|
|
||||||
thread_kwargs: dict | None = None,
|
|
||||||
**_,
|
|
||||||
):
|
|
||||||
"""Signal handler for all object's m2m_changed"""
|
"""Signal handler for all object's m2m_changed"""
|
||||||
if action not in ["pre_add", "pre_remove", "post_clear"]:
|
if action not in ["pre_add", "pre_remove", "post_clear"]:
|
||||||
return
|
return
|
||||||
@ -237,5 +229,4 @@ class AuditMiddleware:
|
|||||||
request,
|
request,
|
||||||
user=user,
|
user=user,
|
||||||
model=model_to_dict(instance),
|
model=model_to_dict(instance),
|
||||||
**thread_kwargs,
|
|
||||||
).run()
|
).run()
|
||||||
|
|||||||
@ -119,7 +119,7 @@ class SystemTask(TenantTask):
|
|||||||
"task_call_kwargs": sanitize_item(kwargs),
|
"task_call_kwargs": sanitize_item(kwargs),
|
||||||
"status": self._status,
|
"status": self._status,
|
||||||
"messages": sanitize_item(self._messages),
|
"messages": sanitize_item(self._messages),
|
||||||
"expires": now() + timedelta(hours=self.result_timeout_hours + 3),
|
"expires": now() + timedelta(hours=self.result_timeout_hours),
|
||||||
"expiring": True,
|
"expiring": True,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@ -203,8 +203,7 @@ class FlowPlanner:
|
|||||||
"f(plan): building plan",
|
"f(plan): building plan",
|
||||||
)
|
)
|
||||||
plan = self._build_plan(user, request, default_context)
|
plan = self._build_plan(user, request, default_context)
|
||||||
if self.use_cache:
|
cache.set(cache_key(self.flow, user), plan, CACHE_TIMEOUT)
|
||||||
cache.set(cache_key(self.flow, user), plan, CACHE_TIMEOUT)
|
|
||||||
if not plan.bindings and not self.allow_empty_flows:
|
if not plan.bindings and not self.allow_empty_flows:
|
||||||
raise EmptyFlowException()
|
raise EmptyFlowException()
|
||||||
return plan
|
return plan
|
||||||
|
|||||||
@ -53,7 +53,6 @@ cache:
|
|||||||
|
|
||||||
# result_backend:
|
# result_backend:
|
||||||
# url: ""
|
# url: ""
|
||||||
# transport_options: ""
|
|
||||||
|
|
||||||
debug: false
|
debug: false
|
||||||
remote_debug: false
|
remote_debug: false
|
||||||
@ -96,6 +95,9 @@ outposts:
|
|||||||
discover: true
|
discover: true
|
||||||
disable_embedded_outpost: false
|
disable_embedded_outpost: false
|
||||||
|
|
||||||
|
expressions:
|
||||||
|
global_runtime: python # or python_restricted
|
||||||
|
|
||||||
ldap:
|
ldap:
|
||||||
task_timeout_hours: 2
|
task_timeout_hours: 2
|
||||||
page_size: 50
|
page_size: 50
|
||||||
|
|||||||
@ -3,27 +3,117 @@
|
|||||||
import re
|
import re
|
||||||
import socket
|
import socket
|
||||||
from collections.abc import Iterable
|
from collections.abc import Iterable
|
||||||
|
from datetime import timedelta
|
||||||
|
from functools import lru_cache
|
||||||
from ipaddress import ip_address, ip_network
|
from ipaddress import ip_address, ip_network
|
||||||
|
from pathlib import Path
|
||||||
|
from tempfile import gettempdir
|
||||||
from textwrap import indent
|
from textwrap import indent
|
||||||
from typing import Any
|
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 cachetools import TLRUCache, cached
|
||||||
|
from django.conf import settings
|
||||||
from django.core.exceptions import FieldError
|
from django.core.exceptions import FieldError
|
||||||
|
from django.utils.timezone import now
|
||||||
from guardian.shortcuts import get_anonymous_user
|
from guardian.shortcuts import get_anonymous_user
|
||||||
|
from jwt import PyJWTError, decode, encode
|
||||||
from rest_framework.serializers import ValidationError
|
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.hub import Hub
|
||||||
from sentry_sdk.tracing import Span
|
from sentry_sdk.tracing import Span
|
||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
from authentik.core.models import User
|
from authentik.core.models import User
|
||||||
from authentik.events.models import Event
|
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.models import Policy, PolicyBinding
|
||||||
from authentik.policies.process import PolicyProcess
|
from authentik.policies.process import PolicyProcess
|
||||||
from authentik.policies.types import PolicyRequest, PolicyResult
|
from authentik.policies.types import PolicyRequest, PolicyResult
|
||||||
from authentik.stages.authenticator import devices_for_user
|
from authentik.stages.authenticator import devices_for_user
|
||||||
|
|
||||||
LOGGER = get_logger()
|
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:
|
class BaseEvaluator:
|
||||||
@ -37,8 +127,14 @@ class BaseEvaluator:
|
|||||||
# Filename used for exec
|
# Filename used for exec
|
||||||
_filename: str
|
_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._filename = filename if filename else "BaseEvaluator"
|
||||||
|
self._user = user
|
||||||
# update website/docs/expressions/_objects.md
|
# update website/docs/expressions/_objects.md
|
||||||
# update website/docs/expressions/_functions.md
|
# update website/docs/expressions/_functions.md
|
||||||
self._globals = {
|
self._globals = {
|
||||||
@ -57,8 +153,44 @@ class BaseEvaluator:
|
|||||||
"resolve_dns": BaseEvaluator.expr_resolve_dns,
|
"resolve_dns": BaseEvaluator.expr_resolve_dns,
|
||||||
"reverse_dns": BaseEvaluator.expr_reverse_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 = {}
|
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))
|
@cached(cache=TLRUCache(maxsize=32, ttu=lambda key, value, now: now + 180))
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def expr_resolve_dns(host: str, ip_version: int | None = None) -> list[str]:
|
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:
|
def compile(self, expression: str) -> Any:
|
||||||
"""Parse expression. Raises SyntaxError or ValueError if the syntax is incorrect."""
|
"""Parse expression. Raises SyntaxError or ValueError if the syntax is incorrect."""
|
||||||
param_keys = self._context.keys()
|
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:
|
def evaluate(self, expression_source: str) -> Any:
|
||||||
"""Parse and evaluate expression. If the syntax is incorrect, a SyntaxError is raised.
|
"""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)
|
self.handle_error(exc, expression_source)
|
||||||
raise exc
|
raise exc
|
||||||
try:
|
try:
|
||||||
|
if CONFIG.get("expressions.global_runtime") == "python_restricted":
|
||||||
|
self._globals["__builtins__"] = {
|
||||||
|
**safe_builtins,
|
||||||
|
**limited_builtins,
|
||||||
|
**utility_builtins,
|
||||||
|
"__import__": _safe_import,
|
||||||
|
}
|
||||||
_locals = self._context
|
_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
|
# 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
|
# available here, and these policies can only be edited by admins, this is a risk
|
||||||
# we're willing to take.
|
# we're willing to take.
|
||||||
@ -209,6 +360,7 @@ class BaseEvaluator:
|
|||||||
exec(ast_obj, self._globals, _locals) # nosec # noqa
|
exec(ast_obj, self._globals, _locals) # nosec # noqa
|
||||||
result = _locals["result"]
|
result = _locals["result"]
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
|
print(exception_to_string(exc))
|
||||||
# So, this is a bit questionable. Essentially, we are edit the stacktrace
|
# So, this is a bit questionable. Essentially, we are edit the stacktrace
|
||||||
# so the user only sees information relevant to them
|
# so the user only sees information relevant to them
|
||||||
# and none of our surrounding error handling
|
# and none of our surrounding error handling
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
"""Test Evaluator base functions"""
|
"""Test Evaluator base functions"""
|
||||||
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
from guardian.shortcuts import get_anonymous_user
|
||||||
|
|
||||||
from authentik.core.tests.utils import create_test_admin_user
|
from authentik.core.tests.utils import create_test_admin_user
|
||||||
from authentik.events.models import Event
|
from authentik.events.models import Event
|
||||||
@ -33,7 +34,7 @@ class TestEvaluator(TestCase):
|
|||||||
|
|
||||||
def test_expr_event_create(self):
|
def test_expr_event_create(self):
|
||||||
"""Test expr_event_create"""
|
"""Test expr_event_create"""
|
||||||
evaluator = BaseEvaluator(generate_id())
|
evaluator = BaseEvaluator(get_anonymous_user(), generate_id())
|
||||||
evaluator._context = {
|
evaluator._context = {
|
||||||
"foo": "bar",
|
"foo": "bar",
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,12 +14,13 @@ class ExpressionPolicySerializer(PolicySerializer):
|
|||||||
def validate_expression(self, expr: str) -> str:
|
def validate_expression(self, expr: str) -> str:
|
||||||
"""validate the syntax of the expression"""
|
"""validate the syntax of the expression"""
|
||||||
name = "temp-policy" if not self.instance else self.instance.name
|
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
|
return expr
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ExpressionPolicy
|
model = ExpressionPolicy
|
||||||
fields = PolicySerializer.Meta.fields + ["expression"]
|
fields = PolicySerializer.Meta.fields + ["expression", "execution_user"]
|
||||||
|
|
||||||
|
|
||||||
class ExpressionPolicyViewSet(UsedByMixin, ModelViewSet):
|
class ExpressionPolicyViewSet(UsedByMixin, ModelViewSet):
|
||||||
|
|||||||
@ -1,11 +1,12 @@
|
|||||||
"""Authentik policy_expression app config"""
|
"""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"""
|
"""Authentik policy_expression app config"""
|
||||||
|
|
||||||
name = "authentik.policies.expression"
|
name = "authentik.policies.expression"
|
||||||
label = "authentik_policies_expression"
|
label = "authentik_policies_expression"
|
||||||
verbose_name = "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 django.http import HttpRequest
|
||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
|
from authentik.core.models import User
|
||||||
from authentik.flows.planner import PLAN_CONTEXT_SSO
|
from authentik.flows.planner import PLAN_CONTEXT_SSO
|
||||||
from authentik.lib.expression.evaluator import BaseEvaluator
|
from authentik.lib.expression.evaluator import BaseEvaluator
|
||||||
from authentik.policies.exceptions import PolicyException
|
from authentik.policies.exceptions import PolicyException
|
||||||
@ -24,8 +25,8 @@ class PolicyEvaluator(BaseEvaluator):
|
|||||||
|
|
||||||
policy: Optional["ExpressionPolicy"] = None
|
policy: Optional["ExpressionPolicy"] = None
|
||||||
|
|
||||||
def __init__(self, policy_name: str | None = None):
|
def __init__(self, user: User, policy_name: str | None = None):
|
||||||
super().__init__(policy_name or "PolicyEvaluator")
|
super().__init__(user, policy_name or "PolicyEvaluator")
|
||||||
self._messages = []
|
self._messages = []
|
||||||
# update website/docs/expressions/_objects.md
|
# update website/docs/expressions/_objects.md
|
||||||
# update website/docs/expressions/_functions.md
|
# update website/docs/expressions/_functions.md
|
||||||
@ -44,6 +45,8 @@ class PolicyEvaluator(BaseEvaluator):
|
|||||||
if request.http_request:
|
if request.http_request:
|
||||||
self.set_http_request(request.http_request)
|
self.set_http_request(request.http_request)
|
||||||
self._context["request"] = request
|
self._context["request"] = request
|
||||||
|
if not self._user:
|
||||||
|
self._user = request.user
|
||||||
self._context["context"] = request.context
|
self._context["context"] = request.context
|
||||||
|
|
||||||
def set_http_request(self, request: HttpRequest):
|
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):
|
class ExpressionPolicy(Policy):
|
||||||
"""Execute arbitrary Python code to implement custom checks and validation."""
|
"""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()
|
expression = models.TextField()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -26,17 +29,11 @@ class ExpressionPolicy(Policy):
|
|||||||
|
|
||||||
def passes(self, request: PolicyRequest) -> PolicyResult:
|
def passes(self, request: PolicyRequest) -> PolicyResult:
|
||||||
"""Evaluate and render expression. Returns PolicyResult(false) on error."""
|
"""Evaluate and render expression. Returns PolicyResult(false) on error."""
|
||||||
evaluator = PolicyEvaluator(self.name)
|
evaluator = PolicyEvaluator(self.execution_user, self.name)
|
||||||
evaluator.policy = self
|
evaluator.policy = self
|
||||||
evaluator.set_policy_request(request)
|
evaluator.set_policy_request(request)
|
||||||
return evaluator.evaluate(self.expression)
|
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):
|
class Meta(Policy.PolicyMeta):
|
||||||
verbose_name = _("Expression Policy")
|
verbose_name = _("Expression Policy")
|
||||||
verbose_name_plural = _("Expression Policies")
|
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):
|
def test_valid(self):
|
||||||
"""test simple value expression"""
|
"""test simple value expression"""
|
||||||
template = "return True"
|
template = "return True"
|
||||||
evaluator = PolicyEvaluator("test")
|
evaluator = PolicyEvaluator(self.request.user, "test")
|
||||||
evaluator.set_policy_request(self.request)
|
evaluator.set_policy_request(self.request)
|
||||||
self.assertEqual(evaluator.evaluate(template).passing, True)
|
self.assertEqual(evaluator.evaluate(template).passing, True)
|
||||||
|
|
||||||
def test_messages(self):
|
def test_messages(self):
|
||||||
"""test expression with message return"""
|
"""test expression with message return"""
|
||||||
template = 'ak_message("some message");return False'
|
template = 'ak_message("some message");return False'
|
||||||
evaluator = PolicyEvaluator("test")
|
evaluator = PolicyEvaluator(self.request.user, "test")
|
||||||
evaluator.set_policy_request(self.request)
|
evaluator.set_policy_request(self.request)
|
||||||
result = evaluator.evaluate(template)
|
result = evaluator.evaluate(template)
|
||||||
self.assertEqual(result.passing, False)
|
self.assertEqual(result.passing, False)
|
||||||
@ -57,7 +57,7 @@ class TestEvaluator(TestCase):
|
|||||||
def test_invalid_syntax(self):
|
def test_invalid_syntax(self):
|
||||||
"""test invalid syntax"""
|
"""test invalid syntax"""
|
||||||
template = ";"
|
template = ";"
|
||||||
evaluator = PolicyEvaluator("test")
|
evaluator = PolicyEvaluator(self.request.user, "test")
|
||||||
evaluator.set_policy_request(self.request)
|
evaluator.set_policy_request(self.request)
|
||||||
with self.assertRaises(PolicyException):
|
with self.assertRaises(PolicyException):
|
||||||
evaluator.evaluate(template)
|
evaluator.evaluate(template)
|
||||||
@ -65,14 +65,14 @@ class TestEvaluator(TestCase):
|
|||||||
def test_validate(self):
|
def test_validate(self):
|
||||||
"""test validate"""
|
"""test validate"""
|
||||||
template = "True"
|
template = "True"
|
||||||
evaluator = PolicyEvaluator("test")
|
evaluator = PolicyEvaluator(self.request.user, "test")
|
||||||
result = evaluator.validate(template)
|
result = evaluator.validate(template)
|
||||||
self.assertEqual(result, True)
|
self.assertEqual(result, True)
|
||||||
|
|
||||||
def test_validate_invalid(self):
|
def test_validate_invalid(self):
|
||||||
"""test validate"""
|
"""test validate"""
|
||||||
template = ";"
|
template = ";"
|
||||||
evaluator = PolicyEvaluator("test")
|
evaluator = PolicyEvaluator(self.request.user, "test")
|
||||||
with self.assertRaises(ValidationError):
|
with self.assertRaises(ValidationError):
|
||||||
evaluator.validate(template)
|
evaluator.validate(template)
|
||||||
|
|
||||||
@ -83,7 +83,7 @@ class TestEvaluator(TestCase):
|
|||||||
execution_logging=True,
|
execution_logging=True,
|
||||||
expression="ak_message(request.http_request.path)\nreturn 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)
|
evaluator.set_policy_request(self.request)
|
||||||
proc = PolicyProcess(PolicyBinding(policy=expr), request=self.request, connection=None)
|
proc = PolicyProcess(PolicyBinding(policy=expr), request=self.request, connection=None)
|
||||||
res = proc.profiling_wrapper()
|
res = proc.profiling_wrapper()
|
||||||
@ -96,16 +96,13 @@ class TestEvaluator(TestCase):
|
|||||||
execution_logging=True,
|
execution_logging=True,
|
||||||
expression="ak_message(request.http_request.path)\nreturn True",
|
expression="ak_message(request.http_request.path)\nreturn True",
|
||||||
)
|
)
|
||||||
tmpl = (
|
tmpl = f"""
|
||||||
"""
|
|
||||||
ak_message(request.http_request.path)
|
ak_message(request.http_request.path)
|
||||||
res = ak_call_policy('%s')
|
res = ak_call_policy('{expr.name}')
|
||||||
ak_message(request.http_request.path)
|
ak_message(request.http_request.path)
|
||||||
for msg in res.messages:
|
for msg in res.messages:
|
||||||
ak_message(msg)
|
ak_message(msg)
|
||||||
"""
|
"""
|
||||||
% expr.name
|
|
||||||
)
|
|
||||||
evaluator = PolicyEvaluator("test")
|
evaluator = PolicyEvaluator("test")
|
||||||
evaluator.set_policy_request(self.request)
|
evaluator.set_policy_request(self.request)
|
||||||
res = evaluator.evaluate(tmpl)
|
res = evaluator.evaluate(tmpl)
|
||||||
|
|||||||
@ -4,10 +4,9 @@ from urllib.parse import urlencode
|
|||||||
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from authentik.core.models import Application, Group
|
from authentik.core.models import Application
|
||||||
from authentik.core.tests.utils import create_test_admin_user, create_test_brand, create_test_flow
|
from authentik.core.tests.utils import create_test_admin_user, create_test_brand, create_test_flow
|
||||||
from authentik.lib.generators import generate_id
|
from authentik.lib.generators import generate_id
|
||||||
from authentik.policies.models import PolicyBinding
|
|
||||||
from authentik.providers.oauth2.models import DeviceToken, OAuth2Provider
|
from authentik.providers.oauth2.models import DeviceToken, OAuth2Provider
|
||||||
from authentik.providers.oauth2.tests.utils import OAuthTestCase
|
from authentik.providers.oauth2.tests.utils import OAuthTestCase
|
||||||
from authentik.providers.oauth2.views.device_init import QS_KEY_CODE
|
from authentik.providers.oauth2.views.device_init import QS_KEY_CODE
|
||||||
@ -78,23 +77,3 @@ class TesOAuth2DeviceInit(OAuthTestCase):
|
|||||||
+ "?"
|
+ "?"
|
||||||
+ urlencode({QS_KEY_CODE: token.user_code}),
|
+ urlencode({QS_KEY_CODE: token.user_code}),
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_device_init_denied(self):
|
|
||||||
"""Test device init"""
|
|
||||||
group = Group.objects.create(name="foo")
|
|
||||||
PolicyBinding.objects.create(
|
|
||||||
group=group,
|
|
||||||
target=self.application,
|
|
||||||
order=0,
|
|
||||||
)
|
|
||||||
token = DeviceToken.objects.create(
|
|
||||||
user_code="foo",
|
|
||||||
provider=self.provider,
|
|
||||||
)
|
|
||||||
res = self.client.get(
|
|
||||||
reverse("authentik_providers_oauth2_root:device-login")
|
|
||||||
+ "?"
|
|
||||||
+ urlencode({QS_KEY_CODE: token.user_code})
|
|
||||||
)
|
|
||||||
self.assertEqual(res.status_code, 200)
|
|
||||||
self.assertIn(b"Permission denied", res.content)
|
|
||||||
|
|||||||
@ -10,7 +10,6 @@ from jwt import PyJWKSet
|
|||||||
|
|
||||||
from authentik.core.models import Application
|
from authentik.core.models import Application
|
||||||
from authentik.core.tests.utils import create_test_cert, create_test_flow
|
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.crypto.models import CertificateKeyPair
|
||||||
from authentik.lib.generators import generate_id
|
from authentik.lib.generators import generate_id
|
||||||
from authentik.providers.oauth2.models import OAuth2Provider
|
from authentik.providers.oauth2.models import OAuth2Provider
|
||||||
@ -83,7 +82,7 @@ class TestJWKS(OAuthTestCase):
|
|||||||
client_id="test",
|
client_id="test",
|
||||||
authorization_flow=create_test_flow(),
|
authorization_flow=create_test_flow(),
|
||||||
redirect_uris="http://local.invalid",
|
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)
|
app = Application.objects.create(name="test", slug="test", provider=provider)
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
|
|||||||
@ -11,11 +11,10 @@ from django.views.decorators.csrf import csrf_exempt
|
|||||||
from rest_framework.throttling import AnonRateThrottle
|
from rest_framework.throttling import AnonRateThrottle
|
||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
from authentik.core.models import Application
|
|
||||||
from authentik.lib.config import CONFIG
|
from authentik.lib.config import CONFIG
|
||||||
from authentik.lib.utils.time import timedelta_from_string
|
from authentik.lib.utils.time import timedelta_from_string
|
||||||
from authentik.providers.oauth2.models import DeviceToken, OAuth2Provider
|
from authentik.providers.oauth2.models import DeviceToken, OAuth2Provider
|
||||||
from authentik.providers.oauth2.views.device_init import QS_KEY_CODE
|
from authentik.providers.oauth2.views.device_init import QS_KEY_CODE, get_application
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
|
||||||
@ -38,9 +37,7 @@ class DeviceView(View):
|
|||||||
).first()
|
).first()
|
||||||
if not provider:
|
if not provider:
|
||||||
return HttpResponseBadRequest()
|
return HttpResponseBadRequest()
|
||||||
try:
|
if not get_application(provider):
|
||||||
_ = provider.application
|
|
||||||
except Application.DoesNotExist:
|
|
||||||
return HttpResponseBadRequest()
|
return HttpResponseBadRequest()
|
||||||
self.provider = provider
|
self.provider = provider
|
||||||
self.client_id = client_id
|
self.client_id = client_id
|
||||||
|
|||||||
@ -1,9 +1,8 @@
|
|||||||
"""Device flow views"""
|
"""Device flow views"""
|
||||||
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from django.http import HttpRequest, HttpResponse
|
from django.http import HttpRequest, HttpResponse
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
from django.views import View
|
||||||
from rest_framework.exceptions import ValidationError
|
from rest_framework.exceptions import ValidationError
|
||||||
from rest_framework.fields import CharField, IntegerField
|
from rest_framework.fields import CharField, IntegerField
|
||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
@ -17,8 +16,7 @@ from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_SSO,
|
|||||||
from authentik.flows.stage import ChallengeStageView
|
from authentik.flows.stage import ChallengeStageView
|
||||||
from authentik.flows.views.executor import SESSION_KEY_PLAN
|
from authentik.flows.views.executor import SESSION_KEY_PLAN
|
||||||
from authentik.lib.utils.urls import redirect_with_qs
|
from authentik.lib.utils.urls import redirect_with_qs
|
||||||
from authentik.policies.views import PolicyAccessView
|
from authentik.providers.oauth2.models import DeviceToken, OAuth2Provider
|
||||||
from authentik.providers.oauth2.models import DeviceToken
|
|
||||||
from authentik.providers.oauth2.views.device_finish import (
|
from authentik.providers.oauth2.views.device_finish import (
|
||||||
PLAN_CONTEXT_DEVICE,
|
PLAN_CONTEXT_DEVICE,
|
||||||
OAuthDeviceCodeFinishStage,
|
OAuthDeviceCodeFinishStage,
|
||||||
@ -33,52 +31,60 @@ LOGGER = get_logger()
|
|||||||
QS_KEY_CODE = "code" # nosec
|
QS_KEY_CODE = "code" # nosec
|
||||||
|
|
||||||
|
|
||||||
class CodeValidatorView(PolicyAccessView):
|
def get_application(provider: OAuth2Provider) -> Application | None:
|
||||||
"""Helper to validate frontside token"""
|
"""Get application from provider"""
|
||||||
|
try:
|
||||||
def __init__(self, code: str, **kwargs: Any) -> None:
|
app = provider.application
|
||||||
super().__init__(**kwargs)
|
if not app:
|
||||||
self.code = code
|
|
||||||
|
|
||||||
def resolve_provider_application(self):
|
|
||||||
self.token = DeviceToken.objects.filter(user_code=self.code).first()
|
|
||||||
if not self.token:
|
|
||||||
raise Application.DoesNotExist
|
|
||||||
self.provider = self.token.provider
|
|
||||||
self.application = self.token.provider.application
|
|
||||||
|
|
||||||
def get(self, request: HttpRequest, *args, **kwargs):
|
|
||||||
scope_descriptions = UserInfoView().get_scope_descriptions(self.token.scope, self.provider)
|
|
||||||
planner = FlowPlanner(self.provider.authorization_flow)
|
|
||||||
planner.allow_empty_flows = True
|
|
||||||
planner.use_cache = False
|
|
||||||
try:
|
|
||||||
plan = planner.plan(
|
|
||||||
request,
|
|
||||||
{
|
|
||||||
PLAN_CONTEXT_SSO: True,
|
|
||||||
PLAN_CONTEXT_APPLICATION: self.application,
|
|
||||||
# OAuth2 related params
|
|
||||||
PLAN_CONTEXT_DEVICE: self.token,
|
|
||||||
# Consent related params
|
|
||||||
PLAN_CONTEXT_CONSENT_HEADER: _("You're about to sign into %(application)s.")
|
|
||||||
% {"application": self.application.name},
|
|
||||||
PLAN_CONTEXT_CONSENT_PERMISSIONS: scope_descriptions,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
except FlowNonApplicableException:
|
|
||||||
LOGGER.warning("Flow not applicable to user")
|
|
||||||
return None
|
return None
|
||||||
plan.insert_stage(in_memory_stage(OAuthDeviceCodeFinishStage))
|
return app
|
||||||
request.session[SESSION_KEY_PLAN] = plan
|
except Application.DoesNotExist:
|
||||||
return redirect_with_qs(
|
return None
|
||||||
"authentik_core:if-flow",
|
|
||||||
request.GET,
|
|
||||||
flow_slug=self.token.provider.authorization_flow.slug,
|
def validate_code(code: int, request: HttpRequest) -> HttpResponse | None:
|
||||||
|
"""Validate user token"""
|
||||||
|
token = DeviceToken.objects.filter(
|
||||||
|
user_code=code,
|
||||||
|
).first()
|
||||||
|
if not token:
|
||||||
|
return None
|
||||||
|
|
||||||
|
app = get_application(token.provider)
|
||||||
|
if not app:
|
||||||
|
return None
|
||||||
|
|
||||||
|
scope_descriptions = UserInfoView().get_scope_descriptions(token.scope, token.provider)
|
||||||
|
planner = FlowPlanner(token.provider.authorization_flow)
|
||||||
|
planner.allow_empty_flows = True
|
||||||
|
planner.use_cache = False
|
||||||
|
try:
|
||||||
|
plan = planner.plan(
|
||||||
|
request,
|
||||||
|
{
|
||||||
|
PLAN_CONTEXT_SSO: True,
|
||||||
|
PLAN_CONTEXT_APPLICATION: app,
|
||||||
|
# OAuth2 related params
|
||||||
|
PLAN_CONTEXT_DEVICE: token,
|
||||||
|
# Consent related params
|
||||||
|
PLAN_CONTEXT_CONSENT_HEADER: _("You're about to sign into %(application)s.")
|
||||||
|
% {"application": app.name},
|
||||||
|
PLAN_CONTEXT_CONSENT_PERMISSIONS: scope_descriptions,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
except FlowNonApplicableException:
|
||||||
|
LOGGER.warning("Flow not applicable to user")
|
||||||
|
return None
|
||||||
|
plan.insert_stage(in_memory_stage(OAuthDeviceCodeFinishStage))
|
||||||
|
request.session[SESSION_KEY_PLAN] = plan
|
||||||
|
return redirect_with_qs(
|
||||||
|
"authentik_core:if-flow",
|
||||||
|
request.GET,
|
||||||
|
flow_slug=token.provider.authorization_flow.slug,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class DeviceEntryView(PolicyAccessView):
|
class DeviceEntryView(View):
|
||||||
"""View used to initiate the device-code flow, url entered by endusers"""
|
"""View used to initiate the device-code flow, url entered by endusers"""
|
||||||
|
|
||||||
def dispatch(self, request: HttpRequest) -> HttpResponse:
|
def dispatch(self, request: HttpRequest) -> HttpResponse:
|
||||||
@ -88,9 +94,7 @@ class DeviceEntryView(PolicyAccessView):
|
|||||||
LOGGER.info("Brand has no device code flow configured", brand=brand)
|
LOGGER.info("Brand has no device code flow configured", brand=brand)
|
||||||
return HttpResponse(status=404)
|
return HttpResponse(status=404)
|
||||||
if QS_KEY_CODE in request.GET:
|
if QS_KEY_CODE in request.GET:
|
||||||
validation = CodeValidatorView(request.GET[QS_KEY_CODE], request=request).dispatch(
|
validation = validate_code(request.GET[QS_KEY_CODE], request)
|
||||||
request
|
|
||||||
)
|
|
||||||
if validation:
|
if validation:
|
||||||
return validation
|
return validation
|
||||||
LOGGER.info("Got code from query parameter but no matching token found")
|
LOGGER.info("Got code from query parameter but no matching token found")
|
||||||
@ -127,7 +131,7 @@ class OAuthDeviceCodeChallengeResponse(ChallengeResponse):
|
|||||||
|
|
||||||
def validate_code(self, code: int) -> HttpResponse | None:
|
def validate_code(self, code: int) -> HttpResponse | None:
|
||||||
"""Validate code and save the returned http response"""
|
"""Validate code and save the returned http response"""
|
||||||
response = CodeValidatorView(code, request=self.stage.request).dispatch(self.stage.request)
|
response = validate_code(code, self.stage.request)
|
||||||
if not response:
|
if not response:
|
||||||
raise ValidationError(_("Invalid code"), "invalid")
|
raise ValidationError(_("Invalid code"), "invalid")
|
||||||
return response
|
return response
|
||||||
|
|||||||
@ -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.lib.utils.time import timedelta_string_validator
|
||||||
from authentik.sources.saml.processors.constants import (
|
from authentik.sources.saml.processors.constants import (
|
||||||
DSA_SHA1,
|
DSA_SHA1,
|
||||||
ECDSA_SHA1,
|
|
||||||
ECDSA_SHA256,
|
|
||||||
ECDSA_SHA384,
|
|
||||||
ECDSA_SHA512,
|
|
||||||
RSA_SHA1,
|
RSA_SHA1,
|
||||||
RSA_SHA256,
|
RSA_SHA256,
|
||||||
RSA_SHA384,
|
RSA_SHA384,
|
||||||
@ -96,7 +92,8 @@ class SAMLProvider(Provider):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
digest_algorithm = models.TextField(
|
digest_algorithm = models.CharField(
|
||||||
|
max_length=50,
|
||||||
choices=(
|
choices=(
|
||||||
(SHA1, _("SHA1")),
|
(SHA1, _("SHA1")),
|
||||||
(SHA256, _("SHA256")),
|
(SHA256, _("SHA256")),
|
||||||
@ -105,16 +102,13 @@ class SAMLProvider(Provider):
|
|||||||
),
|
),
|
||||||
default=SHA256,
|
default=SHA256,
|
||||||
)
|
)
|
||||||
signature_algorithm = models.TextField(
|
signature_algorithm = models.CharField(
|
||||||
|
max_length=50,
|
||||||
choices=(
|
choices=(
|
||||||
(RSA_SHA1, _("RSA-SHA1")),
|
(RSA_SHA1, _("RSA-SHA1")),
|
||||||
(RSA_SHA256, _("RSA-SHA256")),
|
(RSA_SHA256, _("RSA-SHA256")),
|
||||||
(RSA_SHA384, _("RSA-SHA384")),
|
(RSA_SHA384, _("RSA-SHA384")),
|
||||||
(RSA_SHA512, _("RSA-SHA512")),
|
(RSA_SHA512, _("RSA-SHA512")),
|
||||||
(ECDSA_SHA1, _("ECDSA-SHA1")),
|
|
||||||
(ECDSA_SHA256, _("ECDSA-SHA256")),
|
|
||||||
(ECDSA_SHA384, _("ECDSA-SHA384")),
|
|
||||||
(ECDSA_SHA512, _("ECDSA-SHA512")),
|
|
||||||
(DSA_SHA1, _("DSA-SHA1")),
|
(DSA_SHA1, _("DSA-SHA1")),
|
||||||
),
|
),
|
||||||
default=RSA_SHA256,
|
default=RSA_SHA256,
|
||||||
|
|||||||
@ -7,14 +7,13 @@ from lxml import etree # nosec
|
|||||||
|
|
||||||
from authentik.core.models import Application
|
from authentik.core.models import Application
|
||||||
from authentik.core.tests.utils import create_test_cert, create_test_flow
|
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.generators import generate_id
|
||||||
from authentik.lib.tests.utils import load_fixture
|
from authentik.lib.tests.utils import load_fixture
|
||||||
from authentik.lib.xml import lxml_from_string
|
from authentik.lib.xml import lxml_from_string
|
||||||
from authentik.providers.saml.models import SAMLBindings, SAMLPropertyMapping, SAMLProvider
|
from authentik.providers.saml.models import SAMLBindings, SAMLPropertyMapping, SAMLProvider
|
||||||
from authentik.providers.saml.processors.metadata import MetadataProcessor
|
from authentik.providers.saml.processors.metadata import MetadataProcessor
|
||||||
from authentik.providers.saml.processors.metadata_parser import ServiceProviderMetadataParser
|
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):
|
class TestServiceProviderMetadataParser(TestCase):
|
||||||
@ -108,41 +107,12 @@ class TestServiceProviderMetadataParser(TestCase):
|
|||||||
load_fixture("fixtures/cert.xml").replace("/apps/user_saml", "")
|
load_fixture("fixtures/cert.xml").replace("/apps/user_saml", "")
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_signature_rsa(self):
|
def test_signature(self):
|
||||||
"""Test signature validation (RSA)"""
|
"""Test signature validation"""
|
||||||
provider = SAMLProvider.objects.create(
|
provider = SAMLProvider.objects.create(
|
||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
authorization_flow=self.flow,
|
authorization_flow=self.flow,
|
||||||
signing_kp=create_test_cert(PrivateKeyAlg.RSA),
|
signing_kp=create_test_cert(),
|
||||||
)
|
|
||||||
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,
|
|
||||||
)
|
)
|
||||||
Application.objects.create(
|
Application.objects.create(
|
||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
|
|||||||
@ -41,7 +41,7 @@ class SCIMGroupClient(SCIMClient[Group, SCIMGroupSchema]):
|
|||||||
if not scim_group:
|
if not scim_group:
|
||||||
self.logger.debug("Group does not exist in SCIM, skipping")
|
self.logger.debug("Group does not exist in SCIM, skipping")
|
||||||
return None
|
return None
|
||||||
response = self._request("DELETE", f"/Groups/{scim_group.scim_id}")
|
response = self._request("DELETE", f"/Groups/{scim_group.id}")
|
||||||
scim_group.delete()
|
scim_group.delete()
|
||||||
return response
|
return response
|
||||||
|
|
||||||
@ -89,7 +89,7 @@ class SCIMGroupClient(SCIMClient[Group, SCIMGroupSchema]):
|
|||||||
for user in connections:
|
for user in connections:
|
||||||
members.append(
|
members.append(
|
||||||
GroupMember(
|
GroupMember(
|
||||||
value=user.scim_id,
|
value=user.id,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if members:
|
if members:
|
||||||
@ -107,19 +107,16 @@ class SCIMGroupClient(SCIMClient[Group, SCIMGroupSchema]):
|
|||||||
exclude_unset=True,
|
exclude_unset=True,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
scim_id = response.get("id")
|
SCIMGroup.objects.create(provider=self.provider, group=group, id=response["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)
|
|
||||||
|
|
||||||
def _update(self, group: Group, connection: SCIMGroup):
|
def _update(self, group: Group, connection: SCIMGroup):
|
||||||
"""Update existing group"""
|
"""Update existing group"""
|
||||||
scim_group = self.to_scim(group)
|
scim_group = self.to_scim(group)
|
||||||
scim_group.id = connection.scim_id
|
scim_group.id = connection.id
|
||||||
try:
|
try:
|
||||||
return self._request(
|
return self._request(
|
||||||
"PUT",
|
"PUT",
|
||||||
f"/Groups/{connection.scim_id}",
|
f"/Groups/{scim_group.id}",
|
||||||
json=scim_group.model_dump(
|
json=scim_group.model_dump(
|
||||||
mode="json",
|
mode="json",
|
||||||
exclude_unset=True,
|
exclude_unset=True,
|
||||||
@ -188,13 +185,13 @@ class SCIMGroupClient(SCIMClient[Group, SCIMGroupSchema]):
|
|||||||
return
|
return
|
||||||
user_ids = list(
|
user_ids = list(
|
||||||
SCIMUser.objects.filter(user__pk__in=users_set, provider=self.provider).values_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:
|
if len(user_ids) < 1:
|
||||||
return
|
return
|
||||||
self._patch(
|
self._patch(
|
||||||
scim_group.scim_id,
|
scim_group.id,
|
||||||
PatchOperation(
|
PatchOperation(
|
||||||
op=PatchOp.add,
|
op=PatchOp.add,
|
||||||
path="members",
|
path="members",
|
||||||
@ -214,13 +211,13 @@ class SCIMGroupClient(SCIMClient[Group, SCIMGroupSchema]):
|
|||||||
return
|
return
|
||||||
user_ids = list(
|
user_ids = list(
|
||||||
SCIMUser.objects.filter(user__pk__in=users_set, provider=self.provider).values_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:
|
if len(user_ids) < 1:
|
||||||
return
|
return
|
||||||
self._patch(
|
self._patch(
|
||||||
scim_group.scim_id,
|
scim_group.id,
|
||||||
PatchOperation(
|
PatchOperation(
|
||||||
op=PatchOp.remove,
|
op=PatchOp.remove,
|
||||||
path="members",
|
path="members",
|
||||||
|
|||||||
@ -9,14 +9,13 @@ from pydanticscim.service_provider import (
|
|||||||
)
|
)
|
||||||
from pydanticscim.user import User as BaseUser
|
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):
|
class User(BaseUser):
|
||||||
"""Modified User schema with added externalId field"""
|
"""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
|
externalId: str | None = None
|
||||||
meta: dict | None = None
|
meta: dict | None = None
|
||||||
|
|
||||||
@ -24,7 +23,9 @@ class User(BaseUser):
|
|||||||
class Group(BaseGroup):
|
class Group(BaseGroup):
|
||||||
"""Modified Group schema with added externalId field"""
|
"""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
|
externalId: str | None = None
|
||||||
meta: dict | None = None
|
meta: dict | None = None
|
||||||
|
|
||||||
|
|||||||
@ -34,7 +34,7 @@ class SCIMUserClient(SCIMClient[User, SCIMUserSchema]):
|
|||||||
if not scim_user:
|
if not scim_user:
|
||||||
self.logger.debug("User does not exist in SCIM, skipping")
|
self.logger.debug("User does not exist in SCIM, skipping")
|
||||||
return None
|
return None
|
||||||
response = self._request("DELETE", f"/Users/{scim_user.scim_id}")
|
response = self._request("DELETE", f"/Users/{scim_user.id}")
|
||||||
scim_user.delete()
|
scim_user.delete()
|
||||||
return response
|
return response
|
||||||
|
|
||||||
@ -85,18 +85,15 @@ class SCIMUserClient(SCIMClient[User, SCIMUserSchema]):
|
|||||||
exclude_unset=True,
|
exclude_unset=True,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
scim_id = response.get("id")
|
SCIMUser.objects.create(provider=self.provider, user=user, id=response["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)
|
|
||||||
|
|
||||||
def _update(self, user: User, connection: SCIMUser):
|
def _update(self, user: User, connection: SCIMUser):
|
||||||
"""Update existing user"""
|
"""Update existing user"""
|
||||||
scim_user = self.to_scim(user)
|
scim_user = self.to_scim(user)
|
||||||
scim_user.id = connection.scim_id
|
scim_user.id = connection.id
|
||||||
self._request(
|
self._request(
|
||||||
"PUT",
|
"PUT",
|
||||||
f"/Users/{connection.scim_id}",
|
f"/Users/{connection.id}",
|
||||||
json=scim_user.model_dump(
|
json=scim_user.model_dump(
|
||||||
mode="json",
|
mode="json",
|
||||||
exclude_unset=True,
|
exclude_unset=True,
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
from authentik.providers.scim.models import SCIMProvider
|
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
|
from authentik.tenants.management import TenantCommand
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
@ -21,4 +21,4 @@ class Command(TenantCommand):
|
|||||||
if not provider:
|
if not provider:
|
||||||
LOGGER.warning("Provider does not exist", name=provider_name)
|
LOGGER.warning("Provider does not exist", name=provider_name)
|
||||||
continue
|
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"""
|
"""SCIM Provider models"""
|
||||||
|
|
||||||
from uuid import uuid4
|
|
||||||
|
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import QuerySet
|
from django.db.models import QuerySet
|
||||||
@ -99,13 +97,12 @@ class SCIMMapping(PropertyMapping):
|
|||||||
class SCIMUser(models.Model):
|
class SCIMUser(models.Model):
|
||||||
"""Mapping of a user and provider to a SCIM user ID"""
|
"""Mapping of a user and provider to a SCIM user ID"""
|
||||||
|
|
||||||
id = models.UUIDField(primary_key=True, editable=False, default=uuid4)
|
id = models.TextField(primary_key=True)
|
||||||
scim_id = models.TextField()
|
|
||||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||||
provider = models.ForeignKey(SCIMProvider, on_delete=models.CASCADE)
|
provider = models.ForeignKey(SCIMProvider, on_delete=models.CASCADE)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = (("scim_id", "user", "provider"),)
|
unique_together = (("id", "user", "provider"),)
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return f"SCIM User {self.user_id} to {self.provider_id}"
|
return f"SCIM User {self.user_id} to {self.provider_id}"
|
||||||
@ -114,13 +111,12 @@ class SCIMUser(models.Model):
|
|||||||
class SCIMGroup(models.Model):
|
class SCIMGroup(models.Model):
|
||||||
"""Mapping of a group and provider to a SCIM user ID"""
|
"""Mapping of a group and provider to a SCIM user ID"""
|
||||||
|
|
||||||
id = models.UUIDField(primary_key=True, editable=False, default=uuid4)
|
id = models.TextField(primary_key=True)
|
||||||
scim_id = models.TextField()
|
|
||||||
group = models.ForeignKey(Group, on_delete=models.CASCADE)
|
group = models.ForeignKey(Group, on_delete=models.CASCADE)
|
||||||
provider = models.ForeignKey(SCIMProvider, on_delete=models.CASCADE)
|
provider = models.ForeignKey(SCIMProvider, on_delete=models.CASCADE)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = (("scim_id", "group", "provider"),)
|
unique_together = (("id", "group", "provider"),)
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return f"SCIM Group {self.group_id} to {self.provider_id}"
|
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.core.models import Group, User
|
||||||
from authentik.lib.utils.reflection import class_to_path
|
from authentik.lib.utils.reflection import class_to_path
|
||||||
from authentik.providers.scim.models import SCIMProvider
|
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()
|
LOGGER = get_logger()
|
||||||
|
|
||||||
@ -17,7 +17,7 @@ LOGGER = get_logger()
|
|||||||
@receiver(post_save, sender=SCIMProvider)
|
@receiver(post_save, sender=SCIMProvider)
|
||||||
def post_save_provider(sender: type[Model], instance, created: bool, **_):
|
def post_save_provider(sender: type[Model], instance, created: bool, **_):
|
||||||
"""Trigger sync when SCIM provider is saved"""
|
"""Trigger sync when SCIM provider is saved"""
|
||||||
scim_task_wrapper(instance.pk)
|
scim_sync.delay(instance.pk)
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_save, sender=User)
|
@receiver(post_save, sender=User)
|
||||||
|
|||||||
@ -38,23 +38,7 @@ def client_for_model(provider: SCIMProvider, model: Model) -> SCIMClient:
|
|||||||
def scim_sync_all():
|
def scim_sync_all():
|
||||||
"""Run sync for all providers"""
|
"""Run sync for all providers"""
|
||||||
for provider in SCIMProvider.objects.filter(backchannel_application__isnull=False):
|
for provider in SCIMProvider.objects.filter(backchannel_application__isnull=False):
|
||||||
scim_task_wrapper(provider.pk)
|
scim_sync.delay(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)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@CELERY_APP.task(bind=True, base=SystemTask)
|
@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)
|
users_paginator = Paginator(provider.get_user_qs(), PAGE_SIZE)
|
||||||
groups_paginator = Paginator(provider.get_group_qs(), PAGE_SIZE)
|
groups_paginator = Paginator(provider.get_group_qs(), PAGE_SIZE)
|
||||||
self.soft_time_limit = self.time_limit = (
|
self.soft_time_limit = self.time_limit = (
|
||||||
users_paginator.num_pages + groups_paginator.num_pages
|
users_paginator.count + groups_paginator.count
|
||||||
) * PAGE_TIMEOUT
|
) * PAGE_TIMEOUT
|
||||||
with allow_join_result():
|
with allow_join_result():
|
||||||
try:
|
try:
|
||||||
|
|||||||
@ -8,7 +8,7 @@ from authentik.core.models import Application, Group, User
|
|||||||
from authentik.lib.generators import generate_id
|
from authentik.lib.generators import generate_id
|
||||||
from authentik.providers.scim.clients.schema import ServiceProviderConfiguration
|
from authentik.providers.scim.clients.schema import ServiceProviderConfiguration
|
||||||
from authentik.providers.scim.models import SCIMMapping, SCIMProvider
|
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
|
from authentik.tenants.models import Tenant
|
||||||
|
|
||||||
|
|
||||||
@ -79,7 +79,7 @@ class SCIMMembershipTests(TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.configure()
|
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.call_count, 6)
|
||||||
self.assertEqual(mocker.request_history[0].method, "GET")
|
self.assertEqual(mocker.request_history[0].method, "GET")
|
||||||
@ -169,7 +169,7 @@ class SCIMMembershipTests(TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.configure()
|
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.call_count, 6)
|
||||||
self.assertEqual(mocker.request_history[0].method, "GET")
|
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.core.models import Application, Group, User
|
||||||
from authentik.lib.generators import generate_id
|
from authentik.lib.generators import generate_id
|
||||||
from authentik.providers.scim.models import SCIMMapping, SCIMProvider
|
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
|
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()
|
@Mocker()
|
||||||
def test_user_create_update(self, mock: Mocker):
|
def test_user_create_update(self, mock: Mocker):
|
||||||
"""Test user creation and update"""
|
"""Test user creation and update"""
|
||||||
@ -302,7 +236,7 @@ class SCIMUserTests(TestCase):
|
|||||||
email=f"{uid}@goauthentik.io",
|
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.call_count, 5)
|
||||||
self.assertEqual(mock.request_history[0].method, "GET")
|
self.assertEqual(mock.request_history[0].method, "GET")
|
||||||
|
|||||||
@ -376,13 +376,7 @@ CELERY = {
|
|||||||
"task_default_queue": "authentik",
|
"task_default_queue": "authentik",
|
||||||
"broker_url": CONFIG.get("broker.url") or redis_url(CONFIG.get("redis.db")),
|
"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")),
|
"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": CONFIG.get_dict_from_b64_json("broker.transport_options"),
|
||||||
"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,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Sentry integration
|
# Sentry integration
|
||||||
|
|||||||
@ -76,7 +76,7 @@ class S3Storage(BaseS3Storage):
|
|||||||
|
|
||||||
return safe_join(self.location, connection.schema_name, name)
|
return safe_join(self.location, connection.schema_name, name)
|
||||||
except ValueError:
|
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
|
# This is a fix for https://github.com/jschneier/django-storages/pull/839
|
||||||
def url(self, name, parameters=None, expire=None, http_method=None):
|
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.lib.utils.time import timedelta_string_validator
|
||||||
from authentik.sources.saml.processors.constants import (
|
from authentik.sources.saml.processors.constants import (
|
||||||
DSA_SHA1,
|
DSA_SHA1,
|
||||||
ECDSA_SHA1,
|
|
||||||
ECDSA_SHA256,
|
|
||||||
ECDSA_SHA384,
|
|
||||||
ECDSA_SHA512,
|
|
||||||
RSA_SHA1,
|
RSA_SHA1,
|
||||||
RSA_SHA256,
|
RSA_SHA256,
|
||||||
RSA_SHA384,
|
RSA_SHA384,
|
||||||
@ -147,7 +143,8 @@ class SAMLSource(Source):
|
|||||||
verbose_name=_("Signing Keypair"),
|
verbose_name=_("Signing Keypair"),
|
||||||
)
|
)
|
||||||
|
|
||||||
digest_algorithm = models.TextField(
|
digest_algorithm = models.CharField(
|
||||||
|
max_length=50,
|
||||||
choices=(
|
choices=(
|
||||||
(SHA1, _("SHA1")),
|
(SHA1, _("SHA1")),
|
||||||
(SHA256, _("SHA256")),
|
(SHA256, _("SHA256")),
|
||||||
@ -156,16 +153,13 @@ class SAMLSource(Source):
|
|||||||
),
|
),
|
||||||
default=SHA256,
|
default=SHA256,
|
||||||
)
|
)
|
||||||
signature_algorithm = models.TextField(
|
signature_algorithm = models.CharField(
|
||||||
|
max_length=50,
|
||||||
choices=(
|
choices=(
|
||||||
(RSA_SHA1, _("RSA-SHA1")),
|
(RSA_SHA1, _("RSA-SHA1")),
|
||||||
(RSA_SHA256, _("RSA-SHA256")),
|
(RSA_SHA256, _("RSA-SHA256")),
|
||||||
(RSA_SHA384, _("RSA-SHA384")),
|
(RSA_SHA384, _("RSA-SHA384")),
|
||||||
(RSA_SHA512, _("RSA-SHA512")),
|
(RSA_SHA512, _("RSA-SHA512")),
|
||||||
(ECDSA_SHA1, _("ECDSA-SHA1")),
|
|
||||||
(ECDSA_SHA256, _("ECDSA-SHA256")),
|
|
||||||
(ECDSA_SHA384, _("ECDSA-SHA384")),
|
|
||||||
(ECDSA_SHA512, _("ECDSA-SHA512")),
|
|
||||||
(DSA_SHA1, _("DSA-SHA1")),
|
(DSA_SHA1, _("DSA-SHA1")),
|
||||||
),
|
),
|
||||||
default=RSA_SHA256,
|
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"
|
DSA_SHA1 = "http://www.w3.org/2000/09/xmldsig#dsa-sha1"
|
||||||
RSA_SHA1 = "http://www.w3.org/2000/09/xmldsig#rsa-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_SHA256 = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"
|
||||||
RSA_SHA384 = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha384"
|
RSA_SHA384 = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha384"
|
||||||
RSA_SHA512 = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512"
|
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"
|
SHA1 = "http://www.w3.org/2000/09/xmldsig#sha1"
|
||||||
SHA256 = "http://www.w3.org/2001/04/xmlenc#sha256"
|
SHA256 = "http://www.w3.org/2001/04/xmlenc#sha256"
|
||||||
@ -48,11 +41,6 @@ SIGN_ALGORITHM_TRANSFORM_MAP = {
|
|||||||
RSA_SHA256: xmlsec.constants.TransformRsaSha256,
|
RSA_SHA256: xmlsec.constants.TransformRsaSha256,
|
||||||
RSA_SHA384: xmlsec.constants.TransformRsaSha384,
|
RSA_SHA384: xmlsec.constants.TransformRsaSha384,
|
||||||
RSA_SHA512: xmlsec.constants.TransformRsaSha512,
|
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 = {
|
DIGEST_ALGORITHM_TRANSLATION_MAP = {
|
||||||
|
|||||||
@ -13,7 +13,6 @@ from rest_framework.request import Request
|
|||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
from authentik.core.models import Group, User
|
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.providers.scim.clients.schema import Group as SCIMGroupModel
|
||||||
from authentik.sources.scim.models import SCIMSourceGroup
|
from authentik.sources.scim.models import SCIMSourceGroup
|
||||||
from authentik.sources.scim.views.v2.base import SCIMView
|
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:
|
def group_to_scim(self, scim_group: SCIMSourceGroup) -> dict:
|
||||||
"""Convert Group to SCIM data"""
|
"""Convert Group to SCIM data"""
|
||||||
payload = SCIMGroupModel(
|
payload = SCIMGroupModel(
|
||||||
schemas=[SCIM_USER_SCHEMA],
|
|
||||||
id=str(scim_group.group.pk),
|
id=str(scim_group.group.pk),
|
||||||
externalId=scim_group.id,
|
externalId=scim_group.id,
|
||||||
displayName=scim_group.group.name,
|
displayName=scim_group.group.name,
|
||||||
members=[],
|
|
||||||
meta={
|
meta={
|
||||||
"resourceType": "Group",
|
"resourceType": "Group",
|
||||||
"location": self.request.build_absolute_uri(
|
"location": self.request.build_absolute_uri(
|
||||||
@ -45,24 +42,28 @@ class GroupsView(SCIMView):
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
for member in scim_group.group.users.order_by("pk"):
|
return payload.model_dump(
|
||||||
member: User
|
mode="json",
|
||||||
payload.members.append(GroupMember(value=str(member.uuid)))
|
exclude_unset=True,
|
||||||
return payload.model_dump(mode="json", exclude_unset=True)
|
)
|
||||||
|
|
||||||
def get(self, request: Request, group_id: str | None = None, **kwargs) -> Response:
|
def get(self, request: Request, group_id: str | None = None, **kwargs) -> Response:
|
||||||
"""List Group handler"""
|
"""List Group handler"""
|
||||||
base_query = SCIMSourceGroup.objects.select_related("group").prefetch_related(
|
|
||||||
"group__users"
|
|
||||||
)
|
|
||||||
if group_id:
|
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:
|
if not connection:
|
||||||
raise Http404
|
raise Http404
|
||||||
return Response(self.group_to_scim(connection))
|
return Response(self.group_to_scim(connection))
|
||||||
connections = (
|
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)
|
page = self.paginate_query(connections)
|
||||||
return Response(
|
return Response(
|
||||||
{
|
{
|
||||||
@ -78,8 +79,6 @@ class GroupsView(SCIMView):
|
|||||||
def update_group(self, connection: SCIMSourceGroup | None, data: QueryDict):
|
def update_group(self, connection: SCIMSourceGroup | None, data: QueryDict):
|
||||||
"""Partial update a group"""
|
"""Partial update a group"""
|
||||||
group = connection.group if connection else Group()
|
group = connection.group if connection else Group()
|
||||||
if _group := Group.objects.filter(name=data.get("displayName")).first():
|
|
||||||
group = _group
|
|
||||||
if "displayName" in data:
|
if "displayName" in data:
|
||||||
group.name = data.get("displayName")
|
group.name = data.get("displayName")
|
||||||
if group.name == "":
|
if group.name == "":
|
||||||
|
|||||||
@ -11,7 +11,6 @@ from rest_framework.request import Request
|
|||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
from authentik.core.models import User
|
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.providers.scim.clients.schema import User as SCIMUserModel
|
||||||
from authentik.sources.scim.models import SCIMSourceUser
|
from authentik.sources.scim.models import SCIMSourceUser
|
||||||
from authentik.sources.scim.views.v2.base import SCIMView
|
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:
|
def user_to_scim(self, scim_user: SCIMSourceUser) -> dict:
|
||||||
"""Convert User to SCIM data"""
|
"""Convert User to SCIM data"""
|
||||||
payload = SCIMUserModel(
|
payload = SCIMUserModel(
|
||||||
schemas=[SCIM_USER_SCHEMA],
|
|
||||||
id=str(scim_user.user.uuid),
|
id=str(scim_user.user.uuid),
|
||||||
externalId=scim_user.id,
|
externalId=scim_user.id,
|
||||||
userName=scim_user.user.username,
|
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)
|
final_payload.update(scim_user.attributes)
|
||||||
return final_payload
|
return final_payload
|
||||||
|
|
||||||
@ -98,8 +99,6 @@ class UsersView(SCIMView):
|
|||||||
def update_user(self, connection: SCIMSourceUser | None, data: QueryDict):
|
def update_user(self, connection: SCIMSourceUser | None, data: QueryDict):
|
||||||
"""Partial update a user"""
|
"""Partial update a user"""
|
||||||
user = connection.user if connection else 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()
|
user.path = self.source.get_user_path()
|
||||||
if "userName" in data:
|
if "userName" in data:
|
||||||
user.username = data.get("userName")
|
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 (
|
from tenant_schemas_celery.scheduler import (
|
||||||
TenantAwarePersistentScheduler as BaseTenantAwarePersistentScheduler,
|
TenantAwarePersistentScheduler as BaseTenantAwarePersistentScheduler,
|
||||||
)
|
)
|
||||||
from tenant_schemas_celery.scheduler import TenantAwareScheduleEntry
|
|
||||||
|
|
||||||
|
|
||||||
class TenantAwarePersistentScheduler(BaseTenantAwarePersistentScheduler):
|
class TenantAwarePersistentScheduler(BaseTenantAwarePersistentScheduler):
|
||||||
@ -12,11 +11,3 @@ class TenantAwarePersistentScheduler(BaseTenantAwarePersistentScheduler):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def get_queryset(cls):
|
def get_queryset(cls):
|
||||||
return super().get_queryset().filter(ready=True)
|
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
|
model: authentik_stages_prompt.prompt
|
||||||
- attrs:
|
- attrs:
|
||||||
expression: |
|
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")
|
prompt_data = request.context.get("prompt_data")
|
||||||
|
user_group_attributes = request.user.group_attributes(request.http_request)
|
||||||
if not request.user.group_attributes(request.http_request).get(
|
if not user_group_attributes.get(USER_ATTRIBUTE_CHANGE_EMAIL, request.http_request.tenant.default_user_change_email):
|
||||||
USER_ATTRIBUTE_CHANGE_EMAIL, request.http_request.tenant.default_user_change_email
|
|
||||||
):
|
|
||||||
if prompt_data.get("email") != request.user.email:
|
if prompt_data.get("email") != request.user.email:
|
||||||
ak_message("Not allowed to change email address.")
|
ak_message("Not allowed to change email address.")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if not request.user.group_attributes(request.http_request).get(
|
if not user_group_attributes.get(USER_ATTRIBUTE_CHANGE_NAME, request.http_request.tenant.default_user_change_name):
|
||||||
USER_ATTRIBUTE_CHANGE_NAME, request.http_request.tenant.default_user_change_name
|
|
||||||
):
|
|
||||||
if prompt_data.get("name") != request.user.name:
|
if prompt_data.get("name") != request.user.name:
|
||||||
ak_message("Not allowed to change name.")
|
ak_message("Not allowed to change name.")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if not request.user.group_attributes(request.http_request).get(
|
if not user_group_attributes.get(USER_ATTRIBUTE_CHANGE_USERNAME, request.http_request.tenant.default_user_change_username):
|
||||||
USER_ATTRIBUTE_CHANGE_USERNAME, request.http_request.tenant.default_user_change_username
|
|
||||||
):
|
|
||||||
if prompt_data.get("username") != request.user.username:
|
if prompt_data.get("username") != request.user.username:
|
||||||
ak_message("Not allowed to change username.")
|
ak_message("Not allowed to change username.")
|
||||||
return False
|
return False
|
||||||
|
|||||||
@ -89,10 +89,9 @@ entries:
|
|||||||
expression: |
|
expression: |
|
||||||
# This policy ensures that the setup flow can only be
|
# This policy ensures that the setup flow can only be
|
||||||
# used one time
|
# used one time
|
||||||
from authentik.flows.models import Flow, FlowAuthenticationRequirement
|
FlowsApi(api).flows_instances_partial_update("initial-setup", {
|
||||||
Flow.objects.filter(slug="initial-setup").update(
|
"authentication": "REQUIRE_SUPERUSER"
|
||||||
authentication=FlowAuthenticationRequirement.REQUIRE_SUPERUSER,
|
})
|
||||||
)
|
|
||||||
return True
|
return True
|
||||||
id: policy-default-oobe-flow-set-authentication
|
id: policy-default-oobe-flow-set-authentication
|
||||||
identifiers:
|
identifiers:
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
"$schema": "http://json-schema.org/draft-07/schema",
|
"$schema": "http://json-schema.org/draft-07/schema",
|
||||||
"$id": "https://goauthentik.io/blueprints/schema.json",
|
"$id": "https://goauthentik.io/blueprints/schema.json",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"title": "authentik 2024.4.3 Blueprint schema",
|
"title": "authentik 2024.4.1 Blueprint schema",
|
||||||
"required": [
|
"required": [
|
||||||
"version",
|
"version",
|
||||||
"entries"
|
"entries"
|
||||||
@ -3477,6 +3477,10 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"minLength": 1,
|
"minLength": 1,
|
||||||
"title": "Expression"
|
"title": "Expression"
|
||||||
|
},
|
||||||
|
"execution_user": {
|
||||||
|
"type": "integer",
|
||||||
|
"title": "Execution user"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": []
|
"required": []
|
||||||
@ -4131,10 +4135,6 @@
|
|||||||
"http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
|
"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-sha384",
|
||||||
"http://www.w3.org/2001/04/xmldsig-more#rsa-sha512",
|
"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"
|
"http://www.w3.org/2000/09/xmldsig#dsa-sha1"
|
||||||
],
|
],
|
||||||
"title": "Signature algorithm"
|
"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-sha256",
|
||||||
"http://www.w3.org/2001/04/xmldsig-more#rsa-sha384",
|
"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#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"
|
"http://www.w3.org/2000/09/xmldsig#dsa-sha1"
|
||||||
],
|
],
|
||||||
"title": "Signature algorithm"
|
"title": "Signature algorithm"
|
||||||
|
|||||||
@ -32,7 +32,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- redis:/data
|
- redis:/data
|
||||||
server:
|
server:
|
||||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.4.3}
|
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.4.1}
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command: server
|
command: server
|
||||||
environment:
|
environment:
|
||||||
@ -53,7 +53,7 @@ services:
|
|||||||
- postgresql
|
- postgresql
|
||||||
- redis
|
- redis
|
||||||
worker:
|
worker:
|
||||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.4.3}
|
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.4.1}
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command: worker
|
command: worker
|
||||||
environment:
|
environment:
|
||||||
|
|||||||
2
go.mod
2
go.mod
@ -28,7 +28,7 @@ require (
|
|||||||
github.com/spf13/cobra v1.8.0
|
github.com/spf13/cobra v1.8.0
|
||||||
github.com/stretchr/testify v1.9.0
|
github.com/stretchr/testify v1.9.0
|
||||||
github.com/wwt/guac v1.3.2
|
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/exp v0.0.0-20230210204819-062eb4c674ab
|
||||||
golang.org/x/oauth2 v0.19.0
|
golang.org/x/oauth2 v0.19.0
|
||||||
golang.org/x/sync v0.7.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.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 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
|
||||||
go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4=
|
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.2024041.1 h1:oYj6DYqmZJd6/wyknBZLnLa+4+ShT4ry7HQn0W8VXxY=
|
||||||
goauthentik.io/api/v3 v3.2024023.2/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
|
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-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
|||||||
@ -29,4 +29,4 @@ func UserAgent() string {
|
|||||||
return fmt.Sprintf("authentik@%s", FullVersion())
|
return fmt.Sprintf("authentik@%s", FullVersion())
|
||||||
}
|
}
|
||||||
|
|
||||||
const VERSION = "2024.4.3"
|
const VERSION = "2024.4.1"
|
||||||
|
|||||||
@ -117,8 +117,6 @@ def run_migrations():
|
|||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
release_lock(curr)
|
release_lock(curr)
|
||||||
curr.close()
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
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]
|
[tool.poetry]
|
||||||
name = "authentik"
|
name = "authentik"
|
||||||
version = "2024.4.3"
|
version = "2024.4.1"
|
||||||
description = ""
|
description = ""
|
||||||
authors = ["authentik Team <hello@goauthentik.io>"]
|
authors = ["authentik Team <hello@goauthentik.io>"]
|
||||||
|
|
||||||
@ -83,13 +83,13 @@ filterwarnings = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[tool.poetry.dependencies]
|
[tool.poetry.dependencies]
|
||||||
|
authentik_client = "2024.4.1.post1714149882"
|
||||||
argon2-cffi = "*"
|
argon2-cffi = "*"
|
||||||
celery = "*"
|
celery = "*"
|
||||||
channels = { version = "*", extras = ["daphne"] }
|
channels = { version = "*", extras = ["daphne"] }
|
||||||
channels-redis = "*"
|
channels-redis = "*"
|
||||||
codespell = "*"
|
codespell = "*"
|
||||||
colorama = "*"
|
colorama = "*"
|
||||||
cryptography = "*"
|
|
||||||
dacite = "*"
|
dacite = "*"
|
||||||
deepmerge = "*"
|
deepmerge = "*"
|
||||||
defusedxml = "*"
|
defusedxml = "*"
|
||||||
@ -102,7 +102,7 @@ django-redis = "*"
|
|||||||
django-storages = { extras = ["s3"], version = "*" }
|
django-storages = { extras = ["s3"], version = "*" }
|
||||||
# See https://github.com/django-tenants/django-tenants/pull/997
|
# See https://github.com/django-tenants/django-tenants/pull/997
|
||||||
django-tenants = { git = "https://github.com/rissson/django-tenants.git", branch="authentik-fixes" }
|
django-tenants = { git = "https://github.com/rissson/django-tenants.git", branch="authentik-fixes" }
|
||||||
djangorestframework = "3.14.0"
|
djangorestframework = "*"
|
||||||
djangorestframework-guardian = "*"
|
djangorestframework-guardian = "*"
|
||||||
docker = "*"
|
docker = "*"
|
||||||
drf-spectacular = "*"
|
drf-spectacular = "*"
|
||||||
@ -116,17 +116,24 @@ gunicorn = "*"
|
|||||||
jsonpatch = "*"
|
jsonpatch = "*"
|
||||||
kubernetes = "*"
|
kubernetes = "*"
|
||||||
ldap3 = "*"
|
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 = "*" }
|
opencontainers = { extras = ["reggie"], version = "*" }
|
||||||
packaging = "*"
|
packaging = "*"
|
||||||
paramiko = "*"
|
paramiko = "*"
|
||||||
psycopg = { extras = ["c"], version = "*" }
|
psycopg = { extras = ["c"], version = "*" }
|
||||||
|
pycryptodome = "*"
|
||||||
pydantic = "*"
|
pydantic = "*"
|
||||||
pydantic-scim = "*"
|
pydantic-scim = "*"
|
||||||
pyjwt = "*"
|
pyjwt = "*"
|
||||||
python = "~3.12"
|
python = "~3.12"
|
||||||
pyyaml = "*"
|
pyyaml = "*"
|
||||||
requests-oauthlib = "*"
|
requests-oauthlib = "*"
|
||||||
|
restrictedpython = "*"
|
||||||
scim2-filter-parser = "*"
|
scim2-filter-parser = "*"
|
||||||
sentry-sdk = "*"
|
sentry-sdk = "*"
|
||||||
service_identity = "*"
|
service_identity = "*"
|
||||||
|
|||||||
36
schema.yml
36
schema.yml
@ -1,7 +1,7 @@
|
|||||||
openapi: 3.0.3
|
openapi: 3.0.3
|
||||||
info:
|
info:
|
||||||
title: authentik
|
title: authentik
|
||||||
version: 2024.4.3
|
version: 2024.4.1
|
||||||
description: Making authentication simple.
|
description: Making authentication simple.
|
||||||
contact:
|
contact:
|
||||||
email: hello@goauthentik.io
|
email: hello@goauthentik.io
|
||||||
@ -11852,6 +11852,10 @@ paths:
|
|||||||
name: execution_logging
|
name: execution_logging
|
||||||
schema:
|
schema:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
- in: query
|
||||||
|
name: execution_user
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
- in: query
|
- in: query
|
||||||
name: expression
|
name: expression
|
||||||
schema:
|
schema:
|
||||||
@ -17051,10 +17055,6 @@ paths:
|
|||||||
enum:
|
enum:
|
||||||
- http://www.w3.org/2000/09/xmldsig#dsa-sha1
|
- http://www.w3.org/2000/09/xmldsig#dsa-sha1
|
||||||
- http://www.w3.org/2000/09/xmldsig#rsa-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-sha256
|
||||||
- http://www.w3.org/2001/04/xmldsig-more#rsa-sha384
|
- 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#rsa-sha512
|
||||||
@ -20914,10 +20914,6 @@ paths:
|
|||||||
enum:
|
enum:
|
||||||
- http://www.w3.org/2000/09/xmldsig#dsa-sha1
|
- http://www.w3.org/2000/09/xmldsig#dsa-sha1
|
||||||
- http://www.w3.org/2000/09/xmldsig#rsa-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-sha256
|
||||||
- http://www.w3.org/2001/04/xmldsig-more#rsa-sha384
|
- 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#rsa-sha512
|
||||||
@ -30458,11 +30454,6 @@ components:
|
|||||||
- pending_user
|
- pending_user
|
||||||
- pending_user_avatar
|
- pending_user_avatar
|
||||||
- type
|
- type
|
||||||
AlgEnum:
|
|
||||||
enum:
|
|
||||||
- rsa
|
|
||||||
- ecdsa
|
|
||||||
type: string
|
|
||||||
App:
|
App:
|
||||||
type: object
|
type: object
|
||||||
description: Serialize Application info
|
description: Serialize Application info
|
||||||
@ -32120,10 +32111,6 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
validity_days:
|
validity_days:
|
||||||
type: integer
|
type: integer
|
||||||
alg:
|
|
||||||
allOf:
|
|
||||||
- $ref: '#/components/schemas/AlgEnum'
|
|
||||||
default: rsa
|
|
||||||
required:
|
required:
|
||||||
- common_name
|
- common_name
|
||||||
- validity_days
|
- validity_days
|
||||||
@ -33582,6 +33569,9 @@ components:
|
|||||||
readOnly: true
|
readOnly: true
|
||||||
expression:
|
expression:
|
||||||
type: string
|
type: string
|
||||||
|
execution_user:
|
||||||
|
type: integer
|
||||||
|
nullable: true
|
||||||
required:
|
required:
|
||||||
- bound_to
|
- bound_to
|
||||||
- component
|
- component
|
||||||
@ -33605,6 +33595,9 @@ components:
|
|||||||
expression:
|
expression:
|
||||||
type: string
|
type: string
|
||||||
minLength: 1
|
minLength: 1
|
||||||
|
execution_user:
|
||||||
|
type: integer
|
||||||
|
nullable: true
|
||||||
required:
|
required:
|
||||||
- expression
|
- expression
|
||||||
- name
|
- name
|
||||||
@ -38858,6 +38851,9 @@ components:
|
|||||||
expression:
|
expression:
|
||||||
type: string
|
type: string
|
||||||
minLength: 1
|
minLength: 1
|
||||||
|
execution_user:
|
||||||
|
type: integer
|
||||||
|
nullable: true
|
||||||
PatchedFlowRequest:
|
PatchedFlowRequest:
|
||||||
type: object
|
type: object
|
||||||
description: Flow Serializer
|
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-sha256
|
||||||
- http://www.w3.org/2001/04/xmldsig-more#rsa-sha384
|
- 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#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
|
- http://www.w3.org/2000/09/xmldsig#dsa-sha1
|
||||||
type: string
|
type: string
|
||||||
Source:
|
Source:
|
||||||
|
|||||||
146
tests/wdio/package-lock.json
generated
146
tests/wdio/package-lock.json
generated
@ -6,16 +6,16 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "@goauthentik/web-tests",
|
"name": "@goauthentik/web-tests",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chromedriver": "^123.0.4"
|
"chromedriver": "^124.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
|
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.5.0",
|
"@typescript-eslint/eslint-plugin": "^7.5.0",
|
||||||
"@typescript-eslint/parser": "^7.5.0",
|
"@typescript-eslint/parser": "^7.5.0",
|
||||||
"@wdio/cli": "^8.36.0",
|
"@wdio/cli": "^8.36.1",
|
||||||
"@wdio/local-runner": "^8.36.0",
|
"@wdio/local-runner": "^8.36.1",
|
||||||
"@wdio/mocha-framework": "^8.36.0",
|
"@wdio/mocha-framework": "^8.36.1",
|
||||||
"@wdio/spec-reporter": "^8.36.0",
|
"@wdio/spec-reporter": "^8.36.1",
|
||||||
"eslint": "^8.57.0",
|
"eslint": "^8.57.0",
|
||||||
"eslint-config-google": "^0.14.0",
|
"eslint-config-google": "^0.14.0",
|
||||||
"eslint-plugin-sonarjs": "^0.25.1",
|
"eslint-plugin-sonarjs": "^0.25.1",
|
||||||
@ -1189,19 +1189,19 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@wdio/cli": {
|
"node_modules/@wdio/cli": {
|
||||||
"version": "8.36.0",
|
"version": "8.36.1",
|
||||||
"resolved": "https://registry.npmjs.org/@wdio/cli/-/cli-8.36.0.tgz",
|
"resolved": "https://registry.npmjs.org/@wdio/cli/-/cli-8.36.1.tgz",
|
||||||
"integrity": "sha512-B8iEwz9DRzHquPihT74nKUzN9s+rCd1TkBp+JGmdgm7pJqiWTe4FORrzaxWjdiCO78jbYK9LgaMORpCcAzjwIA==",
|
"integrity": "sha512-LZBZiwcvvv5P0HuRXt8IV09UiFT5dnDr1Ag5u2roJL2D7l8wDHHa70PXw9MmlbrnyFCUN3hO7FQVUi9MAsDbDQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/node": "^20.1.1",
|
"@types/node": "^20.1.1",
|
||||||
"@vitest/snapshot": "^1.2.1",
|
"@vitest/snapshot": "^1.2.1",
|
||||||
"@wdio/config": "8.36.0",
|
"@wdio/config": "8.36.1",
|
||||||
"@wdio/globals": "8.36.0",
|
"@wdio/globals": "8.36.1",
|
||||||
"@wdio/logger": "8.28.0",
|
"@wdio/logger": "8.28.0",
|
||||||
"@wdio/protocols": "8.32.0",
|
"@wdio/protocols": "8.32.0",
|
||||||
"@wdio/types": "8.36.0",
|
"@wdio/types": "8.36.1",
|
||||||
"@wdio/utils": "8.36.0",
|
"@wdio/utils": "8.36.1",
|
||||||
"async-exit-hook": "^2.0.1",
|
"async-exit-hook": "^2.0.1",
|
||||||
"chalk": "^5.2.0",
|
"chalk": "^5.2.0",
|
||||||
"chokidar": "^3.5.3",
|
"chokidar": "^3.5.3",
|
||||||
@ -1216,7 +1216,7 @@
|
|||||||
"lodash.union": "^4.6.0",
|
"lodash.union": "^4.6.0",
|
||||||
"read-pkg-up": "10.0.0",
|
"read-pkg-up": "10.0.0",
|
||||||
"recursive-readdir": "^2.2.3",
|
"recursive-readdir": "^2.2.3",
|
||||||
"webdriverio": "8.36.0",
|
"webdriverio": "8.36.1",
|
||||||
"yargs": "^17.7.2"
|
"yargs": "^17.7.2"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
@ -1239,14 +1239,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@wdio/config": {
|
"node_modules/@wdio/config": {
|
||||||
"version": "8.36.0",
|
"version": "8.36.1",
|
||||||
"resolved": "https://registry.npmjs.org/@wdio/config/-/config-8.36.0.tgz",
|
"resolved": "https://registry.npmjs.org/@wdio/config/-/config-8.36.1.tgz",
|
||||||
"integrity": "sha512-sAbqnx/G+OsrMquIncFXjM4U0/E0ULMP0jDHZND75r0e1DYYCHmyacrvIHu3Jyxinl9f6+4XQdev6vqdTqPdNg==",
|
"integrity": "sha512-yCENnym0CrYuLKMJ3fv00WkjCR8QpPqVohGBkq5FvZOZpVJEpoG86Q8l4HtyRnd6ggMTKCA1vTQ/myhbPmZmaQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wdio/logger": "8.28.0",
|
"@wdio/logger": "8.28.0",
|
||||||
"@wdio/types": "8.36.0",
|
"@wdio/types": "8.36.1",
|
||||||
"@wdio/utils": "8.36.0",
|
"@wdio/utils": "8.36.1",
|
||||||
"decamelize": "^6.0.0",
|
"decamelize": "^6.0.0",
|
||||||
"deepmerge-ts": "^5.0.0",
|
"deepmerge-ts": "^5.0.0",
|
||||||
"glob": "^10.2.2",
|
"glob": "^10.2.2",
|
||||||
@ -1257,29 +1257,29 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@wdio/globals": {
|
"node_modules/@wdio/globals": {
|
||||||
"version": "8.36.0",
|
"version": "8.36.1",
|
||||||
"resolved": "https://registry.npmjs.org/@wdio/globals/-/globals-8.36.0.tgz",
|
"resolved": "https://registry.npmjs.org/@wdio/globals/-/globals-8.36.1.tgz",
|
||||||
"integrity": "sha512-vqMq1hR+iF0lqMNJpk9z+QB9l/QfL1DbvOfNhPtQ13NgctfNg42ffuhEObbzTLQN0MftcnPBu6O3pai79y8bUA==",
|
"integrity": "sha512-Qpj6gZCRNxqdVkTwYyi4JdeYO4tLSUj3Ti6yxO0v9A4IRaKW1tS29KUcGgjL9CFSBKAOi2zRY8vvFz1u6ewxtQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^16.13 || >=18"
|
"node": "^16.13 || >=18"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"expect-webdriverio": "^4.11.2",
|
"expect-webdriverio": "^4.11.2",
|
||||||
"webdriverio": "8.36.0"
|
"webdriverio": "8.36.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@wdio/local-runner": {
|
"node_modules/@wdio/local-runner": {
|
||||||
"version": "8.36.0",
|
"version": "8.36.1",
|
||||||
"resolved": "https://registry.npmjs.org/@wdio/local-runner/-/local-runner-8.36.0.tgz",
|
"resolved": "https://registry.npmjs.org/@wdio/local-runner/-/local-runner-8.36.1.tgz",
|
||||||
"integrity": "sha512-MIzbWcXgRQGQQK4H5N39/JFoikOg5cu34l1U6rgw74D6hO79L4RwBg2Oo4TJJYgHUL/4RbVwyeLdb5WDTdluTQ==",
|
"integrity": "sha512-FYsTzbNGRnrniOsLWrZO7+DLecAS9W75AIzFZQVQxruiDFkGmKY5OV6gsuvMlasaqAQXW1s+w29bqrLY4DxdEw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/node": "^20.1.0",
|
"@types/node": "^20.1.0",
|
||||||
"@wdio/logger": "8.28.0",
|
"@wdio/logger": "8.28.0",
|
||||||
"@wdio/repl": "8.24.12",
|
"@wdio/repl": "8.24.12",
|
||||||
"@wdio/runner": "8.36.0",
|
"@wdio/runner": "8.36.1",
|
||||||
"@wdio/types": "8.36.0",
|
"@wdio/types": "8.36.1",
|
||||||
"async-exit-hook": "^2.0.1",
|
"async-exit-hook": "^2.0.1",
|
||||||
"split2": "^4.1.0",
|
"split2": "^4.1.0",
|
||||||
"stream-buffers": "^3.0.2"
|
"stream-buffers": "^3.0.2"
|
||||||
@ -1316,16 +1316,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@wdio/mocha-framework": {
|
"node_modules/@wdio/mocha-framework": {
|
||||||
"version": "8.36.0",
|
"version": "8.36.1",
|
||||||
"resolved": "https://registry.npmjs.org/@wdio/mocha-framework/-/mocha-framework-8.36.0.tgz",
|
"resolved": "https://registry.npmjs.org/@wdio/mocha-framework/-/mocha-framework-8.36.1.tgz",
|
||||||
"integrity": "sha512-5wZgh1apbSKTtgGwvd//L4kxdaXe30AQ3y9YeJD+OuAJUTYFRjTpMS13bO3pX518imQeB8HCm4aUc2kxs7J81Q==",
|
"integrity": "sha512-G0h5AeneMNtoh9CcVQ82OCKj0axxUOEotEcInDu8V6UJbUywNJVL/bdTMKdaq5i84Hnc+s1LUKmLvN95F+lHGA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/mocha": "^10.0.0",
|
"@types/mocha": "^10.0.0",
|
||||||
"@types/node": "^20.1.0",
|
"@types/node": "^20.1.0",
|
||||||
"@wdio/logger": "8.28.0",
|
"@wdio/logger": "8.28.0",
|
||||||
"@wdio/types": "8.36.0",
|
"@wdio/types": "8.36.1",
|
||||||
"@wdio/utils": "8.36.0",
|
"@wdio/utils": "8.36.1",
|
||||||
"mocha": "^10.0.0"
|
"mocha": "^10.0.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@ -1351,14 +1351,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@wdio/reporter": {
|
"node_modules/@wdio/reporter": {
|
||||||
"version": "8.36.0",
|
"version": "8.36.1",
|
||||||
"resolved": "https://registry.npmjs.org/@wdio/reporter/-/reporter-8.36.0.tgz",
|
"resolved": "https://registry.npmjs.org/@wdio/reporter/-/reporter-8.36.1.tgz",
|
||||||
"integrity": "sha512-pkAxqiMC+ljmksOKlK9g6y2NRvrdQiKtxD11rsMwJ6CH4kVDSGIvENw7u3kxg7Qwp0j1rCKf5Hp51npqKQgeDQ==",
|
"integrity": "sha512-HcXr9XKq/6kPC9nexMRXIc/ft3Lvp0yCaW5tps01Axus9wbi5ysLHi2z5sB84F2YdpM+aRf7Lac56xkc4Jldeg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/node": "^20.1.0",
|
"@types/node": "^20.1.0",
|
||||||
"@wdio/logger": "8.28.0",
|
"@wdio/logger": "8.28.0",
|
||||||
"@wdio/types": "8.36.0",
|
"@wdio/types": "8.36.1",
|
||||||
"diff": "^5.0.0",
|
"diff": "^5.0.0",
|
||||||
"object-inspect": "^1.12.0"
|
"object-inspect": "^1.12.0"
|
||||||
},
|
},
|
||||||
@ -1367,35 +1367,35 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@wdio/runner": {
|
"node_modules/@wdio/runner": {
|
||||||
"version": "8.36.0",
|
"version": "8.36.1",
|
||||||
"resolved": "https://registry.npmjs.org/@wdio/runner/-/runner-8.36.0.tgz",
|
"resolved": "https://registry.npmjs.org/@wdio/runner/-/runner-8.36.1.tgz",
|
||||||
"integrity": "sha512-M2ZDL0gmR2VvVMchi3Pkonva6Gn6eFh6IwVCpT0np7zioaqOksy3IM7Aki8kPKKS88Osip5dAfoKIrY7JpHovA==",
|
"integrity": "sha512-bLkxQ46MLEbzIf30adl2nyz8kxED/V0IjcQASm0VKfNmsG8LOf7iOIz+udOF4GkMoF++5JuONA5abUsyLvwatg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/node": "^20.11.28",
|
"@types/node": "^20.11.28",
|
||||||
"@wdio/config": "8.36.0",
|
"@wdio/config": "8.36.1",
|
||||||
"@wdio/globals": "8.36.0",
|
"@wdio/globals": "8.36.1",
|
||||||
"@wdio/logger": "8.28.0",
|
"@wdio/logger": "8.28.0",
|
||||||
"@wdio/types": "8.36.0",
|
"@wdio/types": "8.36.1",
|
||||||
"@wdio/utils": "8.36.0",
|
"@wdio/utils": "8.36.1",
|
||||||
"deepmerge-ts": "^5.1.0",
|
"deepmerge-ts": "^5.1.0",
|
||||||
"expect-webdriverio": "^4.12.0",
|
"expect-webdriverio": "^4.12.0",
|
||||||
"gaze": "^1.1.3",
|
"gaze": "^1.1.3",
|
||||||
"webdriver": "8.36.0",
|
"webdriver": "8.36.1",
|
||||||
"webdriverio": "8.36.0"
|
"webdriverio": "8.36.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^16.13 || >=18"
|
"node": "^16.13 || >=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@wdio/spec-reporter": {
|
"node_modules/@wdio/spec-reporter": {
|
||||||
"version": "8.36.0",
|
"version": "8.36.1",
|
||||||
"resolved": "https://registry.npmjs.org/@wdio/spec-reporter/-/spec-reporter-8.36.0.tgz",
|
"resolved": "https://registry.npmjs.org/@wdio/spec-reporter/-/spec-reporter-8.36.1.tgz",
|
||||||
"integrity": "sha512-GVOiWqVYvzoAo4/4hNVxvyVWVoHyEmAywYhkykyJGL05YpO0oDOZY2kINPePEX5Z+nIsXsiKPmtsGGqWsfQwTw==",
|
"integrity": "sha512-VgAd8VQCfwKYz4A3BPDUYNIQxXhRSTaVNbmDzSlYfo5Jekygk7fz0LRFYBpJ69l7eQH0P5nzEyF92oW/rvE3VA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wdio/reporter": "8.36.0",
|
"@wdio/reporter": "8.36.1",
|
||||||
"@wdio/types": "8.36.0",
|
"@wdio/types": "8.36.1",
|
||||||
"chalk": "^5.1.2",
|
"chalk": "^5.1.2",
|
||||||
"easy-table": "^1.2.0",
|
"easy-table": "^1.2.0",
|
||||||
"pretty-ms": "^7.0.0"
|
"pretty-ms": "^7.0.0"
|
||||||
@ -1417,9 +1417,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@wdio/types": {
|
"node_modules/@wdio/types": {
|
||||||
"version": "8.36.0",
|
"version": "8.36.1",
|
||||||
"resolved": "https://registry.npmjs.org/@wdio/types/-/types-8.36.0.tgz",
|
"resolved": "https://registry.npmjs.org/@wdio/types/-/types-8.36.1.tgz",
|
||||||
"integrity": "sha512-0hw/PaJHqDrbIMvU08w3oMDGg89udSkqWF2hFlGAjOc20quRrhn0F1L+NhFpYdezeRKz5gpgTDIqaQs9RWKq1A==",
|
"integrity": "sha512-kKtyJbypasKo/VQuJ6dTQQwFtHE9qoygjoCZjrQCLGraRSjOEiqZHPR0497wbeCvcgHIYyImbmcylqZNGUE0CQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/node": "^20.1.0"
|
"@types/node": "^20.1.0"
|
||||||
@ -1429,14 +1429,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@wdio/utils": {
|
"node_modules/@wdio/utils": {
|
||||||
"version": "8.36.0",
|
"version": "8.36.1",
|
||||||
"resolved": "https://registry.npmjs.org/@wdio/utils/-/utils-8.36.0.tgz",
|
"resolved": "https://registry.npmjs.org/@wdio/utils/-/utils-8.36.1.tgz",
|
||||||
"integrity": "sha512-3VAbavN206qkvm6lITtOtTgscFChax7shzqHjUNln+QWMRyELtT81iw32ux2ld+Bg3F60LAmhbGodu0lJH7k2w==",
|
"integrity": "sha512-xmgPHU11/o9n2FeRmDFkPRC0okiwA1i2xOcR2c3aSpuk99XkAm9RaMn/6u9LFaqsCpgaVxazcYEGSceO7U4hZA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@puppeteer/browsers": "^1.6.0",
|
"@puppeteer/browsers": "^1.6.0",
|
||||||
"@wdio/logger": "8.28.0",
|
"@wdio/logger": "8.28.0",
|
||||||
"@wdio/types": "8.36.0",
|
"@wdio/types": "8.36.1",
|
||||||
"decamelize": "^6.0.0",
|
"decamelize": "^6.0.0",
|
||||||
"deepmerge-ts": "^5.1.0",
|
"deepmerge-ts": "^5.1.0",
|
||||||
"edgedriver": "^5.3.5",
|
"edgedriver": "^5.3.5",
|
||||||
@ -2084,9 +2084,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/chromedriver": {
|
"node_modules/chromedriver": {
|
||||||
"version": "123.0.4",
|
"version": "124.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-123.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-124.0.1.tgz",
|
||||||
"integrity": "sha512-3Yi7y7q35kkSAOTbRisiww/SL2w+DqafDPAaUShpSuLMmPaOvHQR0i3bm2/33QBiQ8fUb1J/MzppzVL6IDqvhA==",
|
"integrity": "sha512-hxd1tpAUhgMFBZd1h3W7KyMckxofOYCuKAMtcvBDAU0YKKorZcWuq6zP06+Ph0Z1ynPjtgAj0hP9VphCwesjZw==",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@testim/chrome-version": "^1.1.4",
|
"@testim/chrome-version": "^1.1.4",
|
||||||
@ -8886,18 +8886,18 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/webdriver": {
|
"node_modules/webdriver": {
|
||||||
"version": "8.36.0",
|
"version": "8.36.1",
|
||||||
"resolved": "https://registry.npmjs.org/webdriver/-/webdriver-8.36.0.tgz",
|
"resolved": "https://registry.npmjs.org/webdriver/-/webdriver-8.36.1.tgz",
|
||||||
"integrity": "sha512-6fmZI1+OCGbhuGMLBLvA7m9TJvHU1Cyzxqd8rGzIyb8hocR53yh/olfOL1BPcjU1NXmKuU1BePSGF+yiKajiEA==",
|
"integrity": "sha512-547RivYCHStVqtiGQBBcABAkzJbPnAWsxpXGzmj5KL+TOM2JF41N2iQRtUxXqr0jme1Nzzye7WS7Y7iSnK6i1g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/node": "^20.1.0",
|
"@types/node": "^20.1.0",
|
||||||
"@types/ws": "^8.5.3",
|
"@types/ws": "^8.5.3",
|
||||||
"@wdio/config": "8.36.0",
|
"@wdio/config": "8.36.1",
|
||||||
"@wdio/logger": "8.28.0",
|
"@wdio/logger": "8.28.0",
|
||||||
"@wdio/protocols": "8.32.0",
|
"@wdio/protocols": "8.32.0",
|
||||||
"@wdio/types": "8.36.0",
|
"@wdio/types": "8.36.1",
|
||||||
"@wdio/utils": "8.36.0",
|
"@wdio/utils": "8.36.1",
|
||||||
"deepmerge-ts": "^5.1.0",
|
"deepmerge-ts": "^5.1.0",
|
||||||
"got": "^12.6.1",
|
"got": "^12.6.1",
|
||||||
"ky": "^0.33.0",
|
"ky": "^0.33.0",
|
||||||
@ -8908,18 +8908,18 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/webdriverio": {
|
"node_modules/webdriverio": {
|
||||||
"version": "8.36.0",
|
"version": "8.36.1",
|
||||||
"resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-8.36.0.tgz",
|
"resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-8.36.1.tgz",
|
||||||
"integrity": "sha512-4WnEI+OxslHpfSnDXuADaR6bL1M7QxBUEF1mTN56AroOCJelyPvt94yRhszwQnLcJJB2OLn49eUz8M4yBCB51w==",
|
"integrity": "sha512-vzE09oFQeMbOYJ/75jZ13sDIljzC3HH7uoUJKAMAEtyrn/bu1F9Sg/4IDEsvQaRD3pz3ae6SkRld33lcQk6HJA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/node": "^20.1.0",
|
"@types/node": "^20.1.0",
|
||||||
"@wdio/config": "8.36.0",
|
"@wdio/config": "8.36.1",
|
||||||
"@wdio/logger": "8.28.0",
|
"@wdio/logger": "8.28.0",
|
||||||
"@wdio/protocols": "8.32.0",
|
"@wdio/protocols": "8.32.0",
|
||||||
"@wdio/repl": "8.24.12",
|
"@wdio/repl": "8.24.12",
|
||||||
"@wdio/types": "8.36.0",
|
"@wdio/types": "8.36.1",
|
||||||
"@wdio/utils": "8.36.0",
|
"@wdio/utils": "8.36.1",
|
||||||
"archiver": "^7.0.0",
|
"archiver": "^7.0.0",
|
||||||
"aria-query": "^5.0.0",
|
"aria-query": "^5.0.0",
|
||||||
"css-shorthand-properties": "^1.1.1",
|
"css-shorthand-properties": "^1.1.1",
|
||||||
@ -8936,7 +8936,7 @@
|
|||||||
"resq": "^1.9.1",
|
"resq": "^1.9.1",
|
||||||
"rgb2hex": "0.2.5",
|
"rgb2hex": "0.2.5",
|
||||||
"serialize-error": "^11.0.1",
|
"serialize-error": "^11.0.1",
|
||||||
"webdriver": "8.36.0"
|
"webdriver": "8.36.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^16.13 || >=18"
|
"node": "^16.13 || >=18"
|
||||||
|
|||||||
@ -6,10 +6,10 @@
|
|||||||
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
|
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.5.0",
|
"@typescript-eslint/eslint-plugin": "^7.5.0",
|
||||||
"@typescript-eslint/parser": "^7.5.0",
|
"@typescript-eslint/parser": "^7.5.0",
|
||||||
"@wdio/cli": "^8.36.0",
|
"@wdio/cli": "^8.36.1",
|
||||||
"@wdio/local-runner": "^8.36.0",
|
"@wdio/local-runner": "^8.36.1",
|
||||||
"@wdio/mocha-framework": "^8.36.0",
|
"@wdio/mocha-framework": "^8.36.1",
|
||||||
"@wdio/spec-reporter": "^8.36.0",
|
"@wdio/spec-reporter": "^8.36.1",
|
||||||
"eslint": "^8.57.0",
|
"eslint": "^8.57.0",
|
||||||
"eslint-config-google": "^0.14.0",
|
"eslint-config-google": "^0.14.0",
|
||||||
"eslint-plugin-sonarjs": "^0.25.1",
|
"eslint-plugin-sonarjs": "^0.25.1",
|
||||||
@ -32,6 +32,6 @@
|
|||||||
"node": ">=20"
|
"node": ">=20"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"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",
|
"@codemirror/theme-one-dark": "^6.1.2",
|
||||||
"@formatjs/intl-listformat": "^7.5.5",
|
"@formatjs/intl-listformat": "^7.5.5",
|
||||||
"@fortawesome/fontawesome-free": "^6.5.2",
|
"@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-labs/task": "^3.1.0",
|
||||||
"@lit/context": "^1.1.1",
|
"@lit/context": "^1.1.1",
|
||||||
"@lit/localize": "^0.12.1",
|
"@lit/localize": "^0.12.1",
|
||||||
@ -46,7 +46,7 @@
|
|||||||
"@open-wc/lit-helpers": "^0.7.0",
|
"@open-wc/lit-helpers": "^0.7.0",
|
||||||
"@patternfly/elements": "^3.0.1",
|
"@patternfly/elements": "^3.0.1",
|
||||||
"@patternfly/patternfly": "^4.224.2",
|
"@patternfly/patternfly": "^4.224.2",
|
||||||
"@sentry/browser": "^7.111.0",
|
"@sentry/browser": "^7.112.2",
|
||||||
"@webcomponents/webcomponentsjs": "^2.8.0",
|
"@webcomponents/webcomponentsjs": "^2.8.0",
|
||||||
"base64-js": "^1.5.1",
|
"base64-js": "^1.5.1",
|
||||||
"chart.js": "^4.4.2",
|
"chart.js": "^4.4.2",
|
||||||
@ -65,7 +65,7 @@
|
|||||||
"style-mod": "^4.1.2",
|
"style-mod": "^4.1.2",
|
||||||
"ts-pattern": "^5.1.1",
|
"ts-pattern": "^5.1.1",
|
||||||
"webcomponent-qr-code": "^1.2.0",
|
"webcomponent-qr-code": "^1.2.0",
|
||||||
"yaml": "^2.4.1"
|
"yaml": "^2.4.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.24.4",
|
"@babel/core": "^7.24.4",
|
||||||
@ -81,13 +81,13 @@
|
|||||||
"@lit/localize-tools": "^0.7.2",
|
"@lit/localize-tools": "^0.7.2",
|
||||||
"@rollup/plugin-replace": "^5.0.5",
|
"@rollup/plugin-replace": "^5.0.5",
|
||||||
"@spotlightjs/spotlight": "^1.2.17",
|
"@spotlightjs/spotlight": "^1.2.17",
|
||||||
"@storybook/addon-essentials": "^8.0.8",
|
"@storybook/addon-essentials": "^8.0.9",
|
||||||
"@storybook/addon-links": "^8.0.8",
|
"@storybook/addon-links": "^8.0.9",
|
||||||
"@storybook/api": "^7.6.17",
|
"@storybook/api": "^7.6.17",
|
||||||
"@storybook/blocks": "^8.0.8",
|
"@storybook/blocks": "^8.0.8",
|
||||||
"@storybook/manager-api": "^8.0.8",
|
"@storybook/manager-api": "^8.0.9",
|
||||||
"@storybook/web-components": "^8.0.8",
|
"@storybook/web-components": "^8.0.9",
|
||||||
"@storybook/web-components-vite": "^8.0.8",
|
"@storybook/web-components-vite": "^8.0.9",
|
||||||
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
|
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
|
||||||
"@types/chart.js": "^2.9.41",
|
"@types/chart.js": "^2.9.41",
|
||||||
"@types/codemirror": "5.60.15",
|
"@types/codemirror": "5.60.15",
|
||||||
@ -114,10 +114,10 @@
|
|||||||
"prettier": "^3.2.5",
|
"prettier": "^3.2.5",
|
||||||
"pseudolocale": "^2.0.0",
|
"pseudolocale": "^2.0.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.3.1",
|
||||||
"rollup-plugin-modify": "^3.0.0",
|
"rollup-plugin-modify": "^3.0.0",
|
||||||
"rollup-plugin-postcss-lit": "^2.1.0",
|
"rollup-plugin-postcss-lit": "^2.1.0",
|
||||||
"storybook": "^8.0.8",
|
"storybook": "^8.0.9",
|
||||||
"storybook-addon-mock": "^5.0.0",
|
"storybook-addon-mock": "^5.0.0",
|
||||||
"ts-lit-plugin": "^2.0.2",
|
"ts-lit-plugin": "^2.0.2",
|
||||||
"tslib": "^2.6.2",
|
"tslib": "^2.6.2",
|
||||||
@ -129,9 +129,9 @@
|
|||||||
"@esbuild/darwin-arm64": "^0.20.1",
|
"@esbuild/darwin-arm64": "^0.20.1",
|
||||||
"@esbuild/linux-amd64": "^0.18.11",
|
"@esbuild/linux-amd64": "^0.18.11",
|
||||||
"@esbuild/linux-arm64": "^0.20.1",
|
"@esbuild/linux-arm64": "^0.20.1",
|
||||||
"@rollup/rollup-darwin-arm64": "4.14.3",
|
"@rollup/rollup-darwin-arm64": "4.17.0",
|
||||||
"@rollup/rollup-linux-arm64-gnu": "4.14.3",
|
"@rollup/rollup-linux-arm64-gnu": "4.17.0",
|
||||||
"@rollup/rollup-linux-x64-gnu": "4.14.3"
|
"@rollup/rollup-linux-x64-gnu": "4.17.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20"
|
"node": ">=20"
|
||||||
|
|||||||
@ -29,9 +29,5 @@ export const signatureAlgorithmOptions = toOptions([
|
|||||||
["RSA-SHA256", SignatureAlgorithmEnum._200104XmldsigMorersaSha256, true],
|
["RSA-SHA256", SignatureAlgorithmEnum._200104XmldsigMorersaSha256, true],
|
||||||
["RSA-SHA384", SignatureAlgorithmEnum._200104XmldsigMorersaSha384],
|
["RSA-SHA384", SignatureAlgorithmEnum._200104XmldsigMorersaSha384],
|
||||||
["RSA-SHA512", SignatureAlgorithmEnum._200104XmldsigMorersaSha512],
|
["RSA-SHA512", SignatureAlgorithmEnum._200104XmldsigMorersaSha512],
|
||||||
["ECDSA-SHA1", SignatureAlgorithmEnum._200104XmldsigMoreecdsaSha1],
|
|
||||||
["ECDSA-SHA256", SignatureAlgorithmEnum._200104XmldsigMoreecdsaSha256],
|
|
||||||
["ECDSA-SHA384", SignatureAlgorithmEnum._200104XmldsigMoreecdsaSha384],
|
|
||||||
["ECDSA-SHA512", SignatureAlgorithmEnum._200104XmldsigMoreecdsaSha512],
|
|
||||||
["DSA-SHA1", SignatureAlgorithmEnum._200009XmldsigdsaSha1],
|
["DSA-SHA1", SignatureAlgorithmEnum._200009XmldsigdsaSha1],
|
||||||
]);
|
]);
|
||||||
|
|||||||
@ -6,12 +6,7 @@ import { msg } from "@lit/localize";
|
|||||||
import { TemplateResult, html } from "lit";
|
import { TemplateResult, html } from "lit";
|
||||||
import { customElement } from "lit/decorators.js";
|
import { customElement } from "lit/decorators.js";
|
||||||
|
|
||||||
import {
|
import { CertificateGenerationRequest, CertificateKeyPair, CryptoApi } from "@goauthentik/api";
|
||||||
AlgEnum,
|
|
||||||
CertificateGenerationRequest,
|
|
||||||
CertificateKeyPair,
|
|
||||||
CryptoApi,
|
|
||||||
} from "@goauthentik/api";
|
|
||||||
|
|
||||||
@customElement("ak-crypto-certificate-generate-form")
|
@customElement("ak-crypto-certificate-generate-form")
|
||||||
export class CertificateKeyPairForm extends Form<CertificateGenerationRequest> {
|
export class CertificateKeyPairForm extends Form<CertificateGenerationRequest> {
|
||||||
@ -45,29 +40,6 @@ export class CertificateKeyPairForm extends Form<CertificateGenerationRequest> {
|
|||||||
?required=${true}
|
?required=${true}
|
||||||
>
|
>
|
||||||
<input class="pf-c-form-control" type="number" value="365" />
|
<input class="pf-c-form-control" type="number" value="365" />
|
||||||
</ak-form-element-horizontal>
|
</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> `;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -97,7 +97,7 @@ export class EventListPage extends TablePage<Event> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderExpanded(item: Event): TemplateResult {
|
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">
|
<div class="pf-c-table__expandable-row-content">
|
||||||
<ak-event-info .event=${item as EventWithContext}></ak-event-info>
|
<ak-event-info .event=${item as EventWithContext}></ak-event-info>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -12,7 +12,13 @@ import { TemplateResult, html } from "lit";
|
|||||||
import { customElement } from "lit/decorators.js";
|
import { customElement } from "lit/decorators.js";
|
||||||
import { ifDefined } from "lit/directives/if-defined.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")
|
@customElement("ak-policy-expression-form")
|
||||||
export class ExpressionPolicyForm extends BasePolicyForm<ExpressionPolicy> {
|
export class ExpressionPolicyForm extends BasePolicyForm<ExpressionPolicy> {
|
||||||
@ -92,6 +98,39 @@ export class ExpressionPolicyForm extends BasePolicyForm<ExpressionPolicy> {
|
|||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
</ak-form-element-horizontal>
|
</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>
|
</div>
|
||||||
</ak-form-group>`;
|
</ak-form-group>`;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,7 @@ export const SUCCESS_CLASS = "pf-m-success";
|
|||||||
export const ERROR_CLASS = "pf-m-danger";
|
export const ERROR_CLASS = "pf-m-danger";
|
||||||
export const PROGRESS_CLASS = "pf-m-in-progress";
|
export const PROGRESS_CLASS = "pf-m-in-progress";
|
||||||
export const CURRENT_CLASS = "pf-m-current";
|
export const CURRENT_CLASS = "pf-m-current";
|
||||||
export const VERSION = "2024.4.3";
|
export const VERSION = "2024.4.1";
|
||||||
export const TITLE_DEFAULT = "authentik";
|
export const TITLE_DEFAULT = "authentik";
|
||||||
export const ROUTE_SEPARATOR = ";";
|
export const ROUTE_SEPARATOR = ";";
|
||||||
|
|
||||||
|
|||||||
@ -44,7 +44,7 @@ export function docLink(path: string): string {
|
|||||||
const ak = globalAK();
|
const ak = globalAK();
|
||||||
// Default case or beta build which should always point to latest
|
// Default case or beta build which should always point to latest
|
||||||
if (!ak || ak.build !== "") {
|
if (!ak || ak.build !== "") {
|
||||||
return `https://goauthentik.io${path}`;
|
return `https://docs.goauthentik.io${path}`;
|
||||||
}
|
}
|
||||||
return `https://${ak.versionSubdomain}.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 PFList from "@patternfly/patternfly/components/List/list.css";
|
||||||
import PFTable from "@patternfly/patternfly/components/Table/table.css";
|
import PFTable from "@patternfly/patternfly/components/Table/table.css";
|
||||||
import PFFlex from "@patternfly/patternfly/layouts/Flex/flex.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 PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||||
|
|
||||||
import { EventActions, FlowsApi } from "@goauthentik/api";
|
import { EventActions, FlowsApi } from "@goauthentik/api";
|
||||||
@ -82,7 +81,6 @@ export class EventInfo extends AKElement {
|
|||||||
PFCard,
|
PFCard,
|
||||||
PFTable,
|
PFTable,
|
||||||
PFList,
|
PFList,
|
||||||
PFSplit,
|
|
||||||
PFDescriptionList,
|
PFDescriptionList,
|
||||||
css`
|
css`
|
||||||
code {
|
code {
|
||||||
@ -248,17 +246,11 @@ export class EventInfo extends AKElement {
|
|||||||
|
|
||||||
renderModelChanged() {
|
renderModelChanged() {
|
||||||
const diff = this.event.context.diff as unknown as {
|
const diff = this.event.context.diff as unknown as {
|
||||||
[key: string]: {
|
[key: string]: { new_value: unknown; previous_value: unknown };
|
||||||
new_value: unknown;
|
|
||||||
previous_value: unknown;
|
|
||||||
add?: unknown[];
|
|
||||||
remove?: unknown[];
|
|
||||||
clear?: boolean;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
let diffBody = html``;
|
let diffBody = html``;
|
||||||
if (diff) {
|
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>
|
<div class="pf-c-card__title">${msg("Changes made:")}</div>
|
||||||
<table class="pf-c-table pf-m-compact pf-m-grid-md" role="grid">
|
<table class="pf-c-table pf-m-compact pf-m-grid-md" role="grid">
|
||||||
<thead>
|
<thead>
|
||||||
@ -270,36 +262,16 @@ export class EventInfo extends AKElement {
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody role="rowgroup">
|
<tbody role="rowgroup">
|
||||||
${Object.keys(diff).map((key) => {
|
${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">
|
return html` <tr role="row">
|
||||||
<td role="cell"><pre>${key}</pre></td>
|
<td role="cell"><pre>${key}</pre></td>
|
||||||
<td role="cell">
|
<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>
|
||||||
<td role="cell">${newCol}</td>
|
|
||||||
</tr>`;
|
</tr>`;
|
||||||
})}
|
})}
|
||||||
</tbody>
|
</tbody>
|
||||||
@ -308,8 +280,8 @@ ${JSON.stringify(value.new_value, null, 4)}</pre
|
|||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
return html`
|
return html`
|
||||||
<div class="pf-l-split">
|
<div class="pf-l-flex">
|
||||||
<div class="pf-l-split__item pf-m-fill">
|
<div class="pf-l-flex__item">
|
||||||
<div class="pf-c-card__title">${msg("Affected model:")}</div>
|
<div class="pf-c-card__title">${msg("Affected model:")}</div>
|
||||||
<div class="pf-c-card__body">
|
<div class="pf-c-card__body">
|
||||||
${this.getModelInfo(this.event.context?.model as EventModel)}
|
${this.getModelInfo(this.event.context?.model as EventModel)}
|
||||||
|
|||||||
@ -1,23 +1,22 @@
|
|||||||
import { EVENT_REFRESH } from "@goauthentik/authentik/common/constants";
|
import { EVENT_REFRESH } from "@goauthentik/authentik/common/constants";
|
||||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||||
import { authentikBrandContext } from "@goauthentik/elements/AuthentikContexts";
|
import { authentikBrandContext } from "@goauthentik/elements/AuthentikContexts";
|
||||||
|
import type { ReactiveElementHost } from "@goauthentik/elements/types.js";
|
||||||
|
|
||||||
import { ContextProvider } from "@lit/context";
|
import { ContextProvider } from "@lit/context";
|
||||||
import { ReactiveController, ReactiveControllerHost } from "lit";
|
import type { ReactiveController } from "lit";
|
||||||
|
|
||||||
import type { CurrentBrand } from "@goauthentik/api";
|
import type { CurrentBrand } from "@goauthentik/api";
|
||||||
import { CoreApi } from "@goauthentik/api";
|
import { CoreApi } from "@goauthentik/api";
|
||||||
|
|
||||||
import type { AkInterface } from "./Interface";
|
import type { AkInterface } from "./Interface";
|
||||||
|
|
||||||
type ReactiveElementHost = Partial<ReactiveControllerHost> & AkInterface;
|
|
||||||
|
|
||||||
export class BrandContextController implements ReactiveController {
|
export class BrandContextController implements ReactiveController {
|
||||||
host!: ReactiveElementHost;
|
host!: ReactiveElementHost<AkInterface>;
|
||||||
|
|
||||||
context!: ContextProvider<{ __context__: CurrentBrand | undefined }>;
|
context!: ContextProvider<{ __context__: CurrentBrand | undefined }>;
|
||||||
|
|
||||||
constructor(host: ReactiveElementHost) {
|
constructor(host: ReactiveElementHost<AkInterface>) {
|
||||||
this.host = host;
|
this.host = host;
|
||||||
this.context = new ContextProvider(this.host, {
|
this.context = new ContextProvider(this.host, {
|
||||||
context: authentikBrandContext,
|
context: authentikBrandContext,
|
||||||
|
|||||||
@ -2,23 +2,22 @@ import { EVENT_REFRESH } from "@goauthentik/authentik/common/constants";
|
|||||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||||
import { globalAK } from "@goauthentik/common/global";
|
import { globalAK } from "@goauthentik/common/global";
|
||||||
import { authentikConfigContext } from "@goauthentik/elements/AuthentikContexts";
|
import { authentikConfigContext } from "@goauthentik/elements/AuthentikContexts";
|
||||||
|
import type { ReactiveElementHost } from "@goauthentik/elements/types.js";
|
||||||
|
|
||||||
import { ContextProvider } from "@lit/context";
|
import { ContextProvider } from "@lit/context";
|
||||||
import { ReactiveController, ReactiveControllerHost } from "lit";
|
import type { ReactiveController } from "lit";
|
||||||
|
|
||||||
import type { Config } from "@goauthentik/api";
|
import type { Config } from "@goauthentik/api";
|
||||||
import { RootApi } from "@goauthentik/api";
|
import { RootApi } from "@goauthentik/api";
|
||||||
|
|
||||||
import type { AkInterface } from "./Interface";
|
import type { AkInterface } from "./Interface";
|
||||||
|
|
||||||
type ReactiveElementHost = Partial<ReactiveControllerHost> & AkInterface;
|
|
||||||
|
|
||||||
export class ConfigContextController implements ReactiveController {
|
export class ConfigContextController implements ReactiveController {
|
||||||
host!: ReactiveElementHost;
|
host!: ReactiveElementHost<AkInterface>;
|
||||||
|
|
||||||
context!: ContextProvider<{ __context__: Config | undefined }>;
|
context!: ContextProvider<{ __context__: Config | undefined }>;
|
||||||
|
|
||||||
constructor(host: ReactiveElementHost) {
|
constructor(host: ReactiveElementHost<AkInterface>) {
|
||||||
this.host = host;
|
this.host = host;
|
||||||
this.context = new ContextProvider(this.host, {
|
this.context = new ContextProvider(this.host, {
|
||||||
context: authentikConfigContext,
|
context: authentikConfigContext,
|
||||||
|
|||||||
@ -1,23 +1,22 @@
|
|||||||
import { EVENT_REFRESH_ENTERPRISE } from "@goauthentik/authentik/common/constants";
|
import { EVENT_REFRESH_ENTERPRISE } from "@goauthentik/authentik/common/constants";
|
||||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||||
import { authentikEnterpriseContext } from "@goauthentik/elements/AuthentikContexts";
|
import { authentikEnterpriseContext } from "@goauthentik/elements/AuthentikContexts";
|
||||||
|
import type { ReactiveElementHost } from "@goauthentik/elements/types.js";
|
||||||
|
|
||||||
import { ContextProvider } from "@lit/context";
|
import { ContextProvider } from "@lit/context";
|
||||||
import { ReactiveController, ReactiveControllerHost } from "lit";
|
import type { ReactiveController } from "lit";
|
||||||
|
|
||||||
import type { LicenseSummary } from "@goauthentik/api";
|
import type { LicenseSummary } from "@goauthentik/api";
|
||||||
import { EnterpriseApi } from "@goauthentik/api";
|
import { EnterpriseApi } from "@goauthentik/api";
|
||||||
|
|
||||||
import type { AkEnterpriseInterface } from "./Interface";
|
import type { AkEnterpriseInterface } from "./Interface";
|
||||||
|
|
||||||
type ReactiveElementHost = Partial<ReactiveControllerHost> & AkEnterpriseInterface;
|
|
||||||
|
|
||||||
export class EnterpriseContextController implements ReactiveController {
|
export class EnterpriseContextController implements ReactiveController {
|
||||||
host!: ReactiveElementHost;
|
host!: ReactiveElementHost<AkEnterpriseInterface>;
|
||||||
|
|
||||||
context!: ContextProvider<{ __context__: LicenseSummary | undefined }>;
|
context!: ContextProvider<{ __context__: LicenseSummary | undefined }>;
|
||||||
|
|
||||||
constructor(host: ReactiveElementHost) {
|
constructor(host: ReactiveElementHost<AkEnterpriseInterface>) {
|
||||||
this.host = host;
|
this.host = host;
|
||||||
this.context = new ContextProvider(this.host, {
|
this.context = new ContextProvider(this.host, {
|
||||||
context: authentikEnterpriseContext,
|
context: authentikEnterpriseContext,
|
||||||
|
|||||||
@ -1,13 +1,11 @@
|
|||||||
import { authentikConfigContext } from "@goauthentik/elements/AuthentikContexts";
|
import { authentikConfigContext } from "@goauthentik/elements/AuthentikContexts";
|
||||||
|
import type { Constructor } from "@goauthentik/elements/types.js";
|
||||||
|
|
||||||
import { consume } from "@lit/context";
|
import { consume } from "@lit/context";
|
||||||
import type { LitElement } from "lit";
|
import type { LitElement } from "lit";
|
||||||
|
|
||||||
import type { Config } from "@goauthentik/api";
|
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>>(
|
export function WithAuthentikConfig<T extends Constructor<LitElement>>(
|
||||||
superclass: T,
|
superclass: T,
|
||||||
subscribe = true,
|
subscribe = true,
|
||||||
|
|||||||
@ -1,14 +1,12 @@
|
|||||||
import { authentikBrandContext } from "@goauthentik/elements/AuthentikContexts";
|
import { authentikBrandContext } from "@goauthentik/elements/AuthentikContexts";
|
||||||
|
import type { AbstractConstructor } from "@goauthentik/elements/types.js";
|
||||||
|
|
||||||
import { consume } from "@lit/context";
|
import { consume } from "@lit/context";
|
||||||
import type { LitElement } from "lit";
|
import type { LitElement } from "lit";
|
||||||
|
|
||||||
import type { CurrentBrand } from "@goauthentik/api";
|
import type { CurrentBrand } from "@goauthentik/api";
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
export function WithBrandConfig<T extends AbstractConstructor<LitElement>>(
|
||||||
type Constructor<T = object> = abstract new (...args: any[]) => T;
|
|
||||||
|
|
||||||
export function WithBrandConfig<T extends Constructor<LitElement>>(
|
|
||||||
superclass: T,
|
superclass: T,
|
||||||
subscribe = true,
|
subscribe = true,
|
||||||
) {
|
) {
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { authentikConfigContext } from "@goauthentik/elements/AuthentikContexts";
|
import { authentikConfigContext } from "@goauthentik/elements/AuthentikContexts";
|
||||||
|
import type { AbstractConstructor } from "@goauthentik/elements/types.js";
|
||||||
|
|
||||||
import { consume } from "@lit/context";
|
import { consume } from "@lit/context";
|
||||||
import type { LitElement } from "lit";
|
import type { LitElement } from "lit";
|
||||||
@ -6,9 +7,6 @@ import type { LitElement } from "lit";
|
|||||||
import { CapabilitiesEnum } from "@goauthentik/api";
|
import { CapabilitiesEnum } from "@goauthentik/api";
|
||||||
import { Config } 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
|
// 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
|
// means that it's inaccessible to any child class looking for it. It's one of the strongest privacy
|
||||||
// guarantees in JavaScript.
|
// 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,
|
superclass: T,
|
||||||
subscribe = true,
|
subscribe = true,
|
||||||
) {
|
) {
|
||||||
|
|||||||
@ -1,13 +1,11 @@
|
|||||||
import { authentikEnterpriseContext } from "@goauthentik/elements/AuthentikContexts";
|
import { authentikEnterpriseContext } from "@goauthentik/elements/AuthentikContexts";
|
||||||
|
import { Constructor } from "@goauthentik/elements/types.js";
|
||||||
|
|
||||||
import { consume } from "@lit/context";
|
import { consume } from "@lit/context";
|
||||||
import type { LitElement } from "lit";
|
import type { LitElement } from "lit";
|
||||||
|
|
||||||
import type { LicenseSummary } from "@goauthentik/api";
|
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>>(
|
export function WithLicenseSummary<T extends Constructor<LitElement>>(
|
||||||
superclass: T,
|
superclass: T,
|
||||||
subscribe = true,
|
subscribe = true,
|
||||||
|
|||||||
@ -2,9 +2,8 @@ import { AKElement } from "@goauthentik/elements/Base";
|
|||||||
import { CustomEmitterElement } from "@goauthentik/elements/utils/eventEmitter";
|
import { CustomEmitterElement } from "@goauthentik/elements/utils/eventEmitter";
|
||||||
|
|
||||||
import { msg } from "@lit/localize";
|
import { msg } from "@lit/localize";
|
||||||
import { PropertyValues } from "@lit/reactive-element/reactive-element";
|
|
||||||
import { TemplateResult, css, html } from "lit";
|
import { TemplateResult, css, html } from "lit";
|
||||||
import { customElement, property, queryAll, state } from "lit/decorators.js";
|
import { customElement, property, queryAll } from "lit/decorators.js";
|
||||||
import { map } from "lit/directives/map.js";
|
import { map } from "lit/directives/map.js";
|
||||||
|
|
||||||
import PFCheck from "@patternfly/patternfly/components/Check/check.css";
|
import PFCheck from "@patternfly/patternfly/components/Check/check.css";
|
||||||
@ -113,14 +112,10 @@ export class CheckboxGroup extends AkElementWithCustomEvents {
|
|||||||
@queryAll('input[type="checkbox"]')
|
@queryAll('input[type="checkbox"]')
|
||||||
checkboxes!: NodeListOf<HTMLInputElement>;
|
checkboxes!: NodeListOf<HTMLInputElement>;
|
||||||
|
|
||||||
@state()
|
|
||||||
values: string[] = [];
|
|
||||||
|
|
||||||
internals?: ElementInternals;
|
internals?: ElementInternals;
|
||||||
doneFirstUpdate = false;
|
|
||||||
|
|
||||||
json() {
|
get json() {
|
||||||
return this.values;
|
return this.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
private get formValue() {
|
private get formValue() {
|
||||||
@ -129,7 +124,7 @@ export class CheckboxGroup extends AkElementWithCustomEvents {
|
|||||||
}
|
}
|
||||||
const name = this.name;
|
const name = this.name;
|
||||||
const entries = new FormData();
|
const entries = new FormData();
|
||||||
this.values.forEach((v) => entries.append(name, v));
|
this.value.forEach((v) => entries.append(name, v));
|
||||||
return entries;
|
return entries;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -141,14 +136,14 @@ export class CheckboxGroup extends AkElementWithCustomEvents {
|
|||||||
|
|
||||||
onClick(ev: Event) {
|
onClick(ev: Event) {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
this.values = Array.from(this.checkboxes)
|
this.value = Array.from(this.checkboxes)
|
||||||
.filter((checkbox) => checkbox.checked)
|
.filter((checkbox) => checkbox.checked)
|
||||||
.map((checkbox) => checkbox.name);
|
.map((checkbox) => checkbox.name);
|
||||||
this.dispatchCustomEvent("change", this.values);
|
this.dispatchCustomEvent("change", this.value);
|
||||||
this.dispatchCustomEvent("input", this.values);
|
this.dispatchCustomEvent("input", this.value);
|
||||||
if (this.internals) {
|
if (this.internals) {
|
||||||
this.internals.setValidity({});
|
this.internals.setValidity({});
|
||||||
if (this.required && this.values.length === 0) {
|
if (this.required && this.value.length === 0) {
|
||||||
this.internals.setValidity(
|
this.internals.setValidity(
|
||||||
{
|
{
|
||||||
valueMissing: true,
|
valueMissing: true,
|
||||||
@ -159,16 +154,6 @@ export class CheckboxGroup extends AkElementWithCustomEvents {
|
|||||||
}
|
}
|
||||||
this.internals.setFormValue(this.formValue);
|
this.internals.setFormValue(this.formValue);
|
||||||
}
|
}
|
||||||
// Doing a write-back so anyone examining the checkbox.value field will get something
|
|
||||||
// meaningful. Doesn't do anything for anyone, usually, but it's nice to have.
|
|
||||||
this.value = this.values;
|
|
||||||
}
|
|
||||||
|
|
||||||
willUpdate(changed: PropertyValues<this>) {
|
|
||||||
if (changed.has("value") && !this.doneFirstUpdate) {
|
|
||||||
this.doneFirstUpdate = true;
|
|
||||||
this.values = this.value;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
@ -198,7 +183,7 @@ export class CheckboxGroup extends AkElementWithCustomEvents {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const renderOne = ([name, label]: CheckboxPr) => {
|
const renderOne = ([name, label]: CheckboxPr) => {
|
||||||
const selected = this.values.includes(name);
|
const selected = this.value.includes(name);
|
||||||
const blockFwd = (e: Event) => {
|
const blockFwd = (e: Event) => {
|
||||||
e.stopImmediatePropagation();
|
e.stopImmediatePropagation();
|
||||||
};
|
};
|
||||||
|
|||||||
@ -53,9 +53,6 @@ export class AkDualSelectProvider extends CustomListenerElement(AKElement) {
|
|||||||
|
|
||||||
private isLoading = false;
|
private isLoading = false;
|
||||||
|
|
||||||
private doneFirstUpdate = false;
|
|
||||||
private internalSelected: DualSelectPair[] = [];
|
|
||||||
|
|
||||||
private pagination?: Pagination;
|
private pagination?: Pagination;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -72,11 +69,6 @@ export class AkDualSelectProvider extends CustomListenerElement(AKElement) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
willUpdate(changedProperties: PropertyValues<this>) {
|
willUpdate(changedProperties: PropertyValues<this>) {
|
||||||
if (changedProperties.has("selected") && !this.doneFirstUpdate) {
|
|
||||||
this.doneFirstUpdate = true;
|
|
||||||
this.internalSelected = this.selected;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (changedProperties.has("searchDelay")) {
|
if (changedProperties.has("searchDelay")) {
|
||||||
this.doSearch = debounce(
|
this.doSearch = debounce(
|
||||||
AkDualSelectProvider.prototype.doSearch.bind(this),
|
AkDualSelectProvider.prototype.doSearch.bind(this),
|
||||||
@ -113,8 +105,7 @@ export class AkDualSelectProvider extends CustomListenerElement(AKElement) {
|
|||||||
if (!(event instanceof CustomEvent)) {
|
if (!(event instanceof CustomEvent)) {
|
||||||
throw new Error(`Expecting a CustomEvent for change, received ${event} instead`);
|
throw new Error(`Expecting a CustomEvent for change, received ${event} instead`);
|
||||||
}
|
}
|
||||||
this.internalSelected = event.detail.value;
|
this.selected = event.detail.value;
|
||||||
this.selected = this.internalSelected;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onSearch(event: Event) {
|
onSearch(event: Event) {
|
||||||
@ -133,16 +124,12 @@ export class AkDualSelectProvider extends CustomListenerElement(AKElement) {
|
|||||||
return this.dualSelector.value!.selected.map(([k, _]) => k);
|
return this.dualSelector.value!.selected.map(([k, _]) => k);
|
||||||
}
|
}
|
||||||
|
|
||||||
json() {
|
|
||||||
return this.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return html`<ak-dual-select
|
return html`<ak-dual-select
|
||||||
${ref(this.dualSelector)}
|
${ref(this.dualSelector)}
|
||||||
.options=${this.options}
|
.options=${this.options}
|
||||||
.pages=${this.pagination}
|
.pages=${this.pagination}
|
||||||
.selected=${this.internalSelected}
|
.selected=${this.selected}
|
||||||
available-label=${this.availableLabel}
|
available-label=${this.availableLabel}
|
||||||
selected-label=${this.selectedLabel}
|
selected-label=${this.selectedLabel}
|
||||||
></ak-dual-select>`;
|
></ak-dual-select>`;
|
||||||
|
|||||||
@ -80,7 +80,7 @@ export function serializeForm<T extends KeyUnknown>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ("akControl" in inputElement.dataset) {
|
if ("akControl" in inputElement.dataset) {
|
||||||
assignValue(element, (inputElement as unknown as AkControlElement).json(), json);
|
assignValue(element, inputElement.value, json);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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 { BaseStage } from "@goauthentik/flow/stages/base";
|
||||||
|
|
||||||
import { msg, str } from "@lit/localize";
|
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 { customElement, property } from "lit/decorators.js";
|
||||||
import { ifDefined } from "lit/directives/if-defined.js";
|
import { ifDefined } from "lit/directives/if-defined.js";
|
||||||
|
|
||||||
@ -115,16 +115,14 @@ export class WebAuthnAuthenticatorRegisterStage extends BaseStage<
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
updated(changedProperties: PropertyValues<this>) {
|
firstUpdated(): void {
|
||||||
if (changedProperties.has("challenge") && this.challenge !== undefined) {
|
// convert certain members of the PublicKeyCredentialCreateOptions into
|
||||||
// convert certain members of the PublicKeyCredentialCreateOptions into
|
// byte arrays as expected by the spec.
|
||||||
// byte arrays as expected by the spec.
|
this.publicKeyCredentialCreateOptions = transformCredentialCreateOptions(
|
||||||
this.publicKeyCredentialCreateOptions = transformCredentialCreateOptions(
|
this.challenge?.registration as PublicKeyCredentialCreationOptions,
|
||||||
this.challenge?.registration as PublicKeyCredentialCreationOptions,
|
this.challenge?.registration.user.id,
|
||||||
this.challenge?.registration.user.id,
|
);
|
||||||
);
|
this.registerWrapper();
|
||||||
this.registerWrapper();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render(): TemplateResult {
|
render(): TemplateResult {
|
||||||
|
|||||||
@ -6534,6 +6534,15 @@ Bindings to groups/users are checked against the user of the event.</source>
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="sc1673c93148583ba">
|
<trans-unit id="sc1673c93148583ba">
|
||||||
<source>Request failed. Please try again later.</source>
|
<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>
|
</trans-unit>
|
||||||
</body>
|
</body>
|
||||||
</file>
|
</file>
|
||||||
|
|||||||
@ -6803,6 +6803,15 @@ Bindings to groups/users are checked against the user of the event.</source>
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="sc1673c93148583ba">
|
<trans-unit id="sc1673c93148583ba">
|
||||||
<source>Request failed. Please try again later.</source>
|
<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>
|
</trans-unit>
|
||||||
</body>
|
</body>
|
||||||
</file>
|
</file>
|
||||||
|
|||||||
@ -6451,6 +6451,15 @@ Bindings to groups/users are checked against the user of the event.</source>
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="sc1673c93148583ba">
|
<trans-unit id="sc1673c93148583ba">
|
||||||
<source>Request failed. Please try again later.</source>
|
<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>
|
</trans-unit>
|
||||||
</body>
|
</body>
|
||||||
</file>
|
</file>
|
||||||
|
|||||||
@ -8599,6 +8599,15 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="sc1673c93148583ba">
|
<trans-unit id="sc1673c93148583ba">
|
||||||
<source>Request failed. Please try again later.</source>
|
<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>
|
</trans-unit>
|
||||||
</body>
|
</body>
|
||||||
</file>
|
</file>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user