Compare commits
66 Commits
safari-fol
...
version/20
Author | SHA1 | Date | |
---|---|---|---|
748a8e560f | |||
d6c35787b0 | |||
cc214a0eb7 | |||
0c9fd5f056 | |||
92a1f7e01a | |||
1a727b9ea0 | |||
28cc75af29 | |||
0ad245f7f6 | |||
b10957e5df | |||
3adf79c493 | |||
f478593826 | |||
edf4de7271 | |||
db43869e25 | |||
8a668af5f6 | |||
eef233fd11 | |||
833b350c42 | |||
b388265d98 | |||
faefd9776d | |||
a5ee159189 | |||
35c739ee84 | |||
e9764333ea | |||
22af17be2c | |||
679bf17d6f | |||
cbfa51fb31 | |||
5f8c21cc88 | |||
69b3d1722b | |||
fa4ce1d629 | |||
e4a392834f | |||
31fe0e5923 | |||
8b619635ea | |||
1f1db523c0 | |||
bbc23e1d77 | |||
c30b7ee3e9 | |||
2ba79627bc | |||
198cbe1d9d | |||
db6da159d5 | |||
9862e32078 | |||
a7714e2892 | |||
073e1d241b | |||
5c5cc1c7da | |||
3dccce1095 | |||
78f997fbee | |||
ed83c2b0b1 | |||
af780deb27 | |||
a4be38567f | |||
39aafbb34a | |||
07eb5fe533 | |||
301a89dd92 | |||
cd6d0a47f3 | |||
8a23eaef1e | |||
8f285fbcc5 | |||
5d391424f7 | |||
2de11f8a69 | |||
b2dcf94aba | |||
adb532fc5d | |||
5d3b35d1ba | |||
433a94d9ee | |||
f28d622d10 | |||
50a68c22c5 | |||
13c99c8546 | |||
7243add30f | |||
6611a64a62 | |||
5262f61483 | |||
9dcbb4af9e | |||
0665bfac58 | |||
790e0c4d80 |
@ -1,16 +1,16 @@
|
|||||||
[bumpversion]
|
[bumpversion]
|
||||||
current_version = 2024.12.3
|
current_version = 2025.2.3
|
||||||
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*))?
|
||||||
serialize =
|
serialize =
|
||||||
{major}.{minor}.{patch}-{rc_t}{rc_n}
|
{major}.{minor}.{patch}-{rc_t}{rc_n}
|
||||||
{major}.{minor}.{patch}
|
{major}.{minor}.{patch}
|
||||||
message = release: {new_version}
|
message = release: {new_version}
|
||||||
tag_name = version/{new_version}
|
tag_name = version/{new_version}
|
||||||
|
|
||||||
[bumpversion:part:rc_t]
|
[bumpversion:part:rc_t]
|
||||||
values =
|
values =
|
||||||
rc
|
rc
|
||||||
final
|
final
|
||||||
optional_value = final
|
optional_value = final
|
||||||
|
@ -20,8 +20,8 @@ Even if the issue is not a CVE, we still greatly appreciate your help in hardeni
|
|||||||
|
|
||||||
| Version | Supported |
|
| Version | Supported |
|
||||||
| --------- | --------- |
|
| --------- | --------- |
|
||||||
| 2024.10.x | ✅ |
|
|
||||||
| 2024.12.x | ✅ |
|
| 2024.12.x | ✅ |
|
||||||
|
| 2025.2.x | ✅ |
|
||||||
|
|
||||||
## Reporting a Vulnerability
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from os import environ
|
from os import environ
|
||||||
|
|
||||||
__version__ = "2024.12.3"
|
__version__ = "2025.2.3"
|
||||||
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
||||||
|
|
||||||
|
|
||||||
|
@ -59,7 +59,7 @@ class SystemInfoSerializer(PassiveSerializer):
|
|||||||
if not isinstance(value, str):
|
if not isinstance(value, str):
|
||||||
continue
|
continue
|
||||||
actual_value = value
|
actual_value = value
|
||||||
if raw_session in actual_value:
|
if raw_session is not None and raw_session in actual_value:
|
||||||
actual_value = actual_value.replace(
|
actual_value = actual_value.replace(
|
||||||
raw_session, SafeExceptionReporterFilter.cleansed_substitute
|
raw_session, SafeExceptionReporterFilter.cleansed_substitute
|
||||||
)
|
)
|
||||||
|
@ -50,7 +50,6 @@ from authentik.enterprise.providers.microsoft_entra.models import (
|
|||||||
MicrosoftEntraProviderGroup,
|
MicrosoftEntraProviderGroup,
|
||||||
MicrosoftEntraProviderUser,
|
MicrosoftEntraProviderUser,
|
||||||
)
|
)
|
||||||
from authentik.enterprise.providers.rac.models import ConnectionToken
|
|
||||||
from authentik.enterprise.providers.ssf.models import StreamEvent
|
from authentik.enterprise.providers.ssf.models import StreamEvent
|
||||||
from authentik.enterprise.stages.authenticator_endpoint_gdtc.models import (
|
from authentik.enterprise.stages.authenticator_endpoint_gdtc.models import (
|
||||||
EndpointDevice,
|
EndpointDevice,
|
||||||
@ -72,6 +71,7 @@ from authentik.providers.oauth2.models import (
|
|||||||
DeviceToken,
|
DeviceToken,
|
||||||
RefreshToken,
|
RefreshToken,
|
||||||
)
|
)
|
||||||
|
from authentik.providers.rac.models import ConnectionToken
|
||||||
from authentik.providers.scim.models import SCIMProviderGroup, SCIMProviderUser
|
from authentik.providers.scim.models import SCIMProviderGroup, SCIMProviderUser
|
||||||
from authentik.rbac.models import Role
|
from authentik.rbac.models import Role
|
||||||
from authentik.sources.scim.models import SCIMSourceGroup, SCIMSourceUser
|
from authentik.sources.scim.models import SCIMSourceGroup, SCIMSourceUser
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
"""User API Views"""
|
"""User API Views"""
|
||||||
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
from importlib import import_module
|
||||||
from json import loads
|
from json import loads
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
from django.contrib.auth import update_session_auth_hash
|
from django.contrib.auth import update_session_auth_hash
|
||||||
from django.contrib.auth.models import Permission
|
from django.contrib.auth.models import Permission
|
||||||
from django.contrib.sessions.backends.cache import KEY_PREFIX
|
from django.contrib.sessions.backends.base import SessionBase
|
||||||
from django.core.cache import cache
|
|
||||||
from django.db.models.functions import ExtractHour
|
from django.db.models.functions import ExtractHour
|
||||||
from django.db.transaction import atomic
|
from django.db.transaction import atomic
|
||||||
from django.db.utils import IntegrityError
|
from django.db.utils import IntegrityError
|
||||||
@ -91,6 +92,7 @@ from authentik.stages.email.tasks import send_mails
|
|||||||
from authentik.stages.email.utils import TemplateEmailMessage
|
from authentik.stages.email.utils import TemplateEmailMessage
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
SessionStore: SessionBase = import_module(settings.SESSION_ENGINE).SessionStore
|
||||||
|
|
||||||
|
|
||||||
class UserGroupSerializer(ModelSerializer):
|
class UserGroupSerializer(ModelSerializer):
|
||||||
@ -373,7 +375,7 @@ class UsersFilter(FilterSet):
|
|||||||
method="filter_attributes",
|
method="filter_attributes",
|
||||||
)
|
)
|
||||||
|
|
||||||
is_superuser = BooleanFilter(field_name="ak_groups", lookup_expr="is_superuser")
|
is_superuser = BooleanFilter(field_name="ak_groups", method="filter_is_superuser")
|
||||||
uuid = UUIDFilter(field_name="uuid")
|
uuid = UUIDFilter(field_name="uuid")
|
||||||
|
|
||||||
path = CharFilter(field_name="path")
|
path = CharFilter(field_name="path")
|
||||||
@ -391,6 +393,11 @@ class UsersFilter(FilterSet):
|
|||||||
queryset=Group.objects.all().order_by("name"),
|
queryset=Group.objects.all().order_by("name"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def filter_is_superuser(self, queryset, name, value):
|
||||||
|
if value:
|
||||||
|
return queryset.filter(ak_groups__is_superuser=True).distinct()
|
||||||
|
return queryset.exclude(ak_groups__is_superuser=True).distinct()
|
||||||
|
|
||||||
def filter_attributes(self, queryset, name, value):
|
def filter_attributes(self, queryset, name, value):
|
||||||
"""Filter attributes by query args"""
|
"""Filter attributes by query args"""
|
||||||
try:
|
try:
|
||||||
@ -769,7 +776,8 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
|||||||
if not instance.is_active:
|
if not instance.is_active:
|
||||||
sessions = AuthenticatedSession.objects.filter(user=instance)
|
sessions = AuthenticatedSession.objects.filter(user=instance)
|
||||||
session_ids = sessions.values_list("session_key", flat=True)
|
session_ids = sessions.values_list("session_key", flat=True)
|
||||||
cache.delete_many(f"{KEY_PREFIX}{session}" for session in session_ids)
|
for session in session_ids:
|
||||||
|
SessionStore(session).delete()
|
||||||
sessions.delete()
|
sessions.delete()
|
||||||
LOGGER.debug("Deleted user's sessions", user=instance.username)
|
LOGGER.debug("Deleted user's sessions", user=instance.username)
|
||||||
return response
|
return response
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
"""authentik core signals"""
|
"""authentik core signals"""
|
||||||
|
|
||||||
|
from importlib import import_module
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
from django.contrib.auth.signals import user_logged_in, user_logged_out
|
from django.contrib.auth.signals import user_logged_in, user_logged_out
|
||||||
from django.contrib.sessions.backends.cache import KEY_PREFIX
|
from django.contrib.sessions.backends.base import SessionBase
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.core.signals import Signal
|
from django.core.signals import Signal
|
||||||
from django.db.models import Model
|
from django.db.models import Model
|
||||||
@ -25,6 +28,7 @@ password_changed = Signal()
|
|||||||
login_failed = Signal()
|
login_failed = Signal()
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
SessionStore: SessionBase = import_module(settings.SESSION_ENGINE).SessionStore
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_save, sender=Application)
|
@receiver(post_save, sender=Application)
|
||||||
@ -60,8 +64,7 @@ def user_logged_out_session(sender, request: HttpRequest, user: User, **_):
|
|||||||
@receiver(pre_delete, sender=AuthenticatedSession)
|
@receiver(pre_delete, sender=AuthenticatedSession)
|
||||||
def authenticated_session_delete(sender: type[Model], instance: "AuthenticatedSession", **_):
|
def authenticated_session_delete(sender: type[Model], instance: "AuthenticatedSession", **_):
|
||||||
"""Delete session when authenticated session is deleted"""
|
"""Delete session when authenticated session is deleted"""
|
||||||
cache_key = f"{KEY_PREFIX}{instance.session_key}"
|
SessionStore(instance.session_key).delete()
|
||||||
cache.delete(cache_key)
|
|
||||||
|
|
||||||
|
|
||||||
@receiver(pre_save)
|
@receiver(pre_save)
|
||||||
|
@ -35,8 +35,8 @@ from authentik.flows.planner import (
|
|||||||
FlowPlanner,
|
FlowPlanner,
|
||||||
)
|
)
|
||||||
from authentik.flows.stage import StageView
|
from authentik.flows.stage import StageView
|
||||||
from authentik.flows.views.executor import NEXT_ARG_NAME, SESSION_KEY_GET, SESSION_KEY_PLAN
|
from authentik.flows.views.executor import NEXT_ARG_NAME, SESSION_KEY_GET
|
||||||
from authentik.lib.utils.urls import redirect_with_qs
|
from authentik.lib.utils.urls import is_url_absolute
|
||||||
from authentik.lib.views import bad_request_message
|
from authentik.lib.views import bad_request_message
|
||||||
from authentik.policies.denied import AccessDeniedResponse
|
from authentik.policies.denied import AccessDeniedResponse
|
||||||
from authentik.policies.utils import delete_none_values
|
from authentik.policies.utils import delete_none_values
|
||||||
@ -47,8 +47,9 @@ from authentik.stages.user_write.stage import PLAN_CONTEXT_USER_PATH
|
|||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
|
||||||
SESSION_KEY_OVERRIDE_FLOW_TOKEN = "authentik/flows/source_override_flow_token" # nosec
|
|
||||||
PLAN_CONTEXT_SOURCE_GROUPS = "source_groups"
|
PLAN_CONTEXT_SOURCE_GROUPS = "source_groups"
|
||||||
|
SESSION_KEY_SOURCE_FLOW_STAGES = "authentik/flows/source_flow_stages"
|
||||||
|
SESSION_KEY_OVERRIDE_FLOW_TOKEN = "authentik/flows/source_override_flow_token" # nosec
|
||||||
|
|
||||||
|
|
||||||
class MessageStage(StageView):
|
class MessageStage(StageView):
|
||||||
@ -208,6 +209,8 @@ class SourceFlowManager:
|
|||||||
final_redirect = self.request.session.get(SESSION_KEY_GET, {}).get(
|
final_redirect = self.request.session.get(SESSION_KEY_GET, {}).get(
|
||||||
NEXT_ARG_NAME, "authentik_core:if-user"
|
NEXT_ARG_NAME, "authentik_core:if-user"
|
||||||
)
|
)
|
||||||
|
if not is_url_absolute(final_redirect):
|
||||||
|
final_redirect = "authentik_core:if-user"
|
||||||
flow_context.update(
|
flow_context.update(
|
||||||
{
|
{
|
||||||
# Since we authenticate the user by their token, they have no backend set
|
# Since we authenticate the user by their token, they have no backend set
|
||||||
@ -219,28 +222,28 @@ class SourceFlowManager:
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
flow_context.update(self.policy_context)
|
flow_context.update(self.policy_context)
|
||||||
if SESSION_KEY_OVERRIDE_FLOW_TOKEN in self.request.session:
|
|
||||||
token: FlowToken = self.request.session.get(SESSION_KEY_OVERRIDE_FLOW_TOKEN)
|
|
||||||
self._logger.info("Replacing source flow with overridden flow", flow=token.flow.slug)
|
|
||||||
plan = token.plan
|
|
||||||
plan.context[PLAN_CONTEXT_IS_RESTORED] = token
|
|
||||||
plan.context.update(flow_context)
|
|
||||||
for stage in self.get_stages_to_append(flow):
|
|
||||||
plan.append_stage(stage)
|
|
||||||
if stages:
|
|
||||||
for stage in stages:
|
|
||||||
plan.append_stage(stage)
|
|
||||||
self.request.session[SESSION_KEY_PLAN] = plan
|
|
||||||
flow_slug = token.flow.slug
|
|
||||||
token.delete()
|
|
||||||
return redirect_with_qs(
|
|
||||||
"authentik_core:if-flow",
|
|
||||||
self.request.GET,
|
|
||||||
flow_slug=flow_slug,
|
|
||||||
)
|
|
||||||
flow_context.setdefault(PLAN_CONTEXT_REDIRECT, final_redirect)
|
flow_context.setdefault(PLAN_CONTEXT_REDIRECT, final_redirect)
|
||||||
|
|
||||||
if not flow:
|
if not flow:
|
||||||
|
# We only check for the flow token here if we don't have a flow, otherwise we rely on
|
||||||
|
# SESSION_KEY_SOURCE_FLOW_STAGES to delegate the usage of this token and dynamically add
|
||||||
|
# stages that deal with this token to return to another flow
|
||||||
|
if SESSION_KEY_OVERRIDE_FLOW_TOKEN in self.request.session:
|
||||||
|
token: FlowToken = self.request.session.get(SESSION_KEY_OVERRIDE_FLOW_TOKEN)
|
||||||
|
self._logger.info(
|
||||||
|
"Replacing source flow with overridden flow", flow=token.flow.slug
|
||||||
|
)
|
||||||
|
plan = token.plan
|
||||||
|
plan.context[PLAN_CONTEXT_IS_RESTORED] = token
|
||||||
|
plan.context.update(flow_context)
|
||||||
|
for stage in self.get_stages_to_append(flow):
|
||||||
|
plan.append_stage(stage)
|
||||||
|
if stages:
|
||||||
|
for stage in stages:
|
||||||
|
plan.append_stage(stage)
|
||||||
|
redirect = plan.to_redirect(self.request, token.flow)
|
||||||
|
token.delete()
|
||||||
|
return redirect
|
||||||
return bad_request_message(
|
return bad_request_message(
|
||||||
self.request,
|
self.request,
|
||||||
_("Configured flow does not exist."),
|
_("Configured flow does not exist."),
|
||||||
@ -259,6 +262,8 @@ class SourceFlowManager:
|
|||||||
if stages:
|
if stages:
|
||||||
for stage in stages:
|
for stage in stages:
|
||||||
plan.append_stage(stage)
|
plan.append_stage(stage)
|
||||||
|
for stage in self.request.session.get(SESSION_KEY_SOURCE_FLOW_STAGES, []):
|
||||||
|
plan.append_stage(stage)
|
||||||
return plan.to_redirect(self.request, flow)
|
return plan.to_redirect(self.request, flow)
|
||||||
|
|
||||||
def handle_auth(
|
def handle_auth(
|
||||||
@ -295,6 +300,8 @@ class SourceFlowManager:
|
|||||||
# When request isn't authenticated we jump straight to auth
|
# When request isn't authenticated we jump straight to auth
|
||||||
if not self.request.user.is_authenticated:
|
if not self.request.user.is_authenticated:
|
||||||
return self.handle_auth(connection)
|
return self.handle_auth(connection)
|
||||||
|
# When an override flow token exists we actually still use a flow for link
|
||||||
|
# to continue the existing flow we came from
|
||||||
if SESSION_KEY_OVERRIDE_FLOW_TOKEN in self.request.session:
|
if SESSION_KEY_OVERRIDE_FLOW_TOKEN in self.request.session:
|
||||||
return self._prepare_flow(None, connection)
|
return self._prepare_flow(None, connection)
|
||||||
connection.save()
|
connection.save()
|
||||||
|
@ -67,6 +67,8 @@ def clean_expired_models(self: SystemTask):
|
|||||||
raise ImproperlyConfigured(
|
raise ImproperlyConfigured(
|
||||||
"Invalid session_storage setting, allowed values are db and cache"
|
"Invalid session_storage setting, allowed values are db and cache"
|
||||||
)
|
)
|
||||||
|
if CONFIG.get("session_storage", "cache") == "db":
|
||||||
|
DBSessionStore.clear_expired()
|
||||||
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}")
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
build: "{{ build }}",
|
build: "{{ build }}",
|
||||||
api: {
|
api: {
|
||||||
base: "{{ base_url }}",
|
base: "{{ base_url }}",
|
||||||
|
relBase: "{{ base_url_rel }}",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
window.addEventListener("DOMContentLoaded", function () {
|
window.addEventListener("DOMContentLoaded", function () {
|
||||||
|
@ -8,6 +8,8 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
||||||
|
{# Darkreader breaks the site regardless of theme as its not compatible with webcomponents, and we default to a dark theme based on preferred colour-scheme #}
|
||||||
|
<meta name="darkreader-lock">
|
||||||
<title>{% block title %}{% trans title|default:brand.branding_title %}{% endblock %}</title>
|
<title>{% block title %}{% trans title|default:brand.branding_title %}{% endblock %}</title>
|
||||||
<link rel="icon" href="{{ brand.branding_favicon_url }}">
|
<link rel="icon" href="{{ brand.branding_favicon_url }}">
|
||||||
<link rel="shortcut icon" href="{{ brand.branding_favicon_url }}">
|
<link rel="shortcut icon" href="{{ brand.branding_favicon_url }}">
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
"""Test Users API"""
|
"""Test Users API"""
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from json import loads
|
||||||
|
|
||||||
from django.contrib.sessions.backends.cache import KEY_PREFIX
|
from django.contrib.sessions.backends.cache import KEY_PREFIX
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
@ -15,7 +16,11 @@ from authentik.core.models import (
|
|||||||
User,
|
User,
|
||||||
UserTypes,
|
UserTypes,
|
||||||
)
|
)
|
||||||
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.flows.models import FlowDesignation
|
from authentik.flows.models import FlowDesignation
|
||||||
from authentik.lib.generators import generate_id, generate_key
|
from authentik.lib.generators import generate_id, generate_key
|
||||||
from authentik.stages.email.models import EmailStage
|
from authentik.stages.email.models import EmailStage
|
||||||
@ -41,6 +46,32 @@ class TestUsersAPI(APITestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_filter_is_superuser(self):
|
||||||
|
"""Test API filtering by superuser status"""
|
||||||
|
self.client.force_login(self.admin)
|
||||||
|
# Test superuser
|
||||||
|
response = self.client.get(
|
||||||
|
reverse("authentik_api:user-list"),
|
||||||
|
data={
|
||||||
|
"is_superuser": True,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
body = loads(response.content)
|
||||||
|
self.assertEqual(len(body["results"]), 1)
|
||||||
|
self.assertEqual(body["results"][0]["username"], self.admin.username)
|
||||||
|
# Test non-superuser
|
||||||
|
response = self.client.get(
|
||||||
|
reverse("authentik_api:user-list"),
|
||||||
|
data={
|
||||||
|
"is_superuser": False,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
body = loads(response.content)
|
||||||
|
self.assertEqual(len(body["results"]), 1, body)
|
||||||
|
self.assertEqual(body["results"][0]["username"], self.user.username)
|
||||||
|
|
||||||
def test_list_with_groups(self):
|
def test_list_with_groups(self):
|
||||||
"""Test listing with groups"""
|
"""Test listing with groups"""
|
||||||
self.client.force_login(self.admin)
|
self.client.force_login(self.admin)
|
||||||
|
@ -55,7 +55,7 @@ class RedirectToAppLaunch(View):
|
|||||||
)
|
)
|
||||||
except FlowNonApplicableException:
|
except FlowNonApplicableException:
|
||||||
raise Http404 from None
|
raise Http404 from None
|
||||||
plan.insert_stage(in_memory_stage(RedirectToAppStage))
|
plan.append_stage(in_memory_stage(RedirectToAppStage))
|
||||||
return plan.to_redirect(request, flow)
|
return plan.to_redirect(request, flow)
|
||||||
|
|
||||||
|
|
||||||
|
@ -53,6 +53,7 @@ class InterfaceView(TemplateView):
|
|||||||
kwargs["build"] = get_build_hash()
|
kwargs["build"] = get_build_hash()
|
||||||
kwargs["url_kwargs"] = self.kwargs
|
kwargs["url_kwargs"] = self.kwargs
|
||||||
kwargs["base_url"] = self.request.build_absolute_uri(CONFIG.get("web.path", "/"))
|
kwargs["base_url"] = self.request.build_absolute_uri(CONFIG.get("web.path", "/"))
|
||||||
|
kwargs["base_url_rel"] = CONFIG.get("web.path", "/")
|
||||||
return super().get_context_data(**kwargs)
|
return super().get_context_data(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,14 +0,0 @@
|
|||||||
"""RAC app config"""
|
|
||||||
|
|
||||||
from authentik.enterprise.apps import EnterpriseConfig
|
|
||||||
|
|
||||||
|
|
||||||
class AuthentikEnterpriseProviderRAC(EnterpriseConfig):
|
|
||||||
"""authentik enterprise rac app config"""
|
|
||||||
|
|
||||||
name = "authentik.enterprise.providers.rac"
|
|
||||||
label = "authentik_providers_rac"
|
|
||||||
verbose_name = "authentik Enterprise.Providers.RAC"
|
|
||||||
default = True
|
|
||||||
mountpoint = ""
|
|
||||||
ws_mountpoint = "authentik.enterprise.providers.rac.urls"
|
|
@ -16,7 +16,6 @@ TENANT_APPS = [
|
|||||||
"authentik.enterprise.audit",
|
"authentik.enterprise.audit",
|
||||||
"authentik.enterprise.providers.google_workspace",
|
"authentik.enterprise.providers.google_workspace",
|
||||||
"authentik.enterprise.providers.microsoft_entra",
|
"authentik.enterprise.providers.microsoft_entra",
|
||||||
"authentik.enterprise.providers.rac",
|
|
||||||
"authentik.enterprise.providers.ssf",
|
"authentik.enterprise.providers.ssf",
|
||||||
"authentik.enterprise.stages.authenticator_endpoint_gdtc",
|
"authentik.enterprise.stages.authenticator_endpoint_gdtc",
|
||||||
"authentik.enterprise.stages.source",
|
"authentik.enterprise.stages.source",
|
||||||
|
@ -9,13 +9,16 @@ from django.utils.timezone import now
|
|||||||
from guardian.shortcuts import get_anonymous_user
|
from guardian.shortcuts import get_anonymous_user
|
||||||
|
|
||||||
from authentik.core.models import Source, User
|
from authentik.core.models import Source, User
|
||||||
from authentik.core.sources.flow_manager import SESSION_KEY_OVERRIDE_FLOW_TOKEN
|
from authentik.core.sources.flow_manager import (
|
||||||
|
SESSION_KEY_OVERRIDE_FLOW_TOKEN,
|
||||||
|
SESSION_KEY_SOURCE_FLOW_STAGES,
|
||||||
|
)
|
||||||
from authentik.core.types import UILoginButton
|
from authentik.core.types import UILoginButton
|
||||||
from authentik.enterprise.stages.source.models import SourceStage
|
from authentik.enterprise.stages.source.models import SourceStage
|
||||||
from authentik.flows.challenge import Challenge, ChallengeResponse
|
from authentik.flows.challenge import Challenge, ChallengeResponse
|
||||||
from authentik.flows.models import FlowToken
|
from authentik.flows.models import FlowToken, in_memory_stage
|
||||||
from authentik.flows.planner import PLAN_CONTEXT_IS_RESTORED
|
from authentik.flows.planner import PLAN_CONTEXT_IS_RESTORED
|
||||||
from authentik.flows.stage import ChallengeStageView
|
from authentik.flows.stage import ChallengeStageView, StageView
|
||||||
from authentik.lib.utils.time import timedelta_from_string
|
from authentik.lib.utils.time import timedelta_from_string
|
||||||
|
|
||||||
PLAN_CONTEXT_RESUME_TOKEN = "resume_token" # nosec
|
PLAN_CONTEXT_RESUME_TOKEN = "resume_token" # nosec
|
||||||
@ -49,6 +52,7 @@ class SourceStageView(ChallengeStageView):
|
|||||||
def get_challenge(self, *args, **kwargs) -> Challenge:
|
def get_challenge(self, *args, **kwargs) -> Challenge:
|
||||||
resume_token = self.create_flow_token()
|
resume_token = self.create_flow_token()
|
||||||
self.request.session[SESSION_KEY_OVERRIDE_FLOW_TOKEN] = resume_token
|
self.request.session[SESSION_KEY_OVERRIDE_FLOW_TOKEN] = resume_token
|
||||||
|
self.request.session[SESSION_KEY_SOURCE_FLOW_STAGES] = [in_memory_stage(SourceStageFinal)]
|
||||||
return self.login_button.challenge
|
return self.login_button.challenge
|
||||||
|
|
||||||
def create_flow_token(self) -> FlowToken:
|
def create_flow_token(self) -> FlowToken:
|
||||||
@ -77,3 +81,19 @@ class SourceStageView(ChallengeStageView):
|
|||||||
|
|
||||||
def challenge_valid(self, response: ChallengeResponse) -> HttpResponse:
|
def challenge_valid(self, response: ChallengeResponse) -> HttpResponse:
|
||||||
return self.executor.stage_ok()
|
return self.executor.stage_ok()
|
||||||
|
|
||||||
|
|
||||||
|
class SourceStageFinal(StageView):
|
||||||
|
"""Dynamic stage injected in the source flow manager. This is injected in the
|
||||||
|
flow the source flow manager picks (authentication or enrollment), and will run at the end.
|
||||||
|
This stage uses the override flow token to resume execution of the initial flow the
|
||||||
|
source stage is bound to."""
|
||||||
|
|
||||||
|
def dispatch(self, *args, **kwargs):
|
||||||
|
token: FlowToken = self.request.session.get(SESSION_KEY_OVERRIDE_FLOW_TOKEN)
|
||||||
|
self.logger.info("Replacing source flow with overridden flow", flow=token.flow.slug)
|
||||||
|
plan = token.plan
|
||||||
|
plan.context[PLAN_CONTEXT_IS_RESTORED] = token
|
||||||
|
response = plan.to_redirect(self.request, token.flow)
|
||||||
|
token.delete()
|
||||||
|
return response
|
||||||
|
@ -4,7 +4,8 @@ from django.urls import reverse
|
|||||||
|
|
||||||
from authentik.core.tests.utils import create_test_flow, create_test_user
|
from authentik.core.tests.utils import create_test_flow, create_test_user
|
||||||
from authentik.enterprise.stages.source.models import SourceStage
|
from authentik.enterprise.stages.source.models import SourceStage
|
||||||
from authentik.flows.models import FlowDesignation, FlowStageBinding, FlowToken
|
from authentik.enterprise.stages.source.stage import SourceStageFinal
|
||||||
|
from authentik.flows.models import FlowDesignation, FlowStageBinding, FlowToken, in_memory_stage
|
||||||
from authentik.flows.planner import PLAN_CONTEXT_IS_RESTORED, FlowPlan
|
from authentik.flows.planner import PLAN_CONTEXT_IS_RESTORED, FlowPlan
|
||||||
from authentik.flows.tests import FlowTestCase
|
from authentik.flows.tests import FlowTestCase
|
||||||
from authentik.flows.views.executor import SESSION_KEY_PLAN
|
from authentik.flows.views.executor import SESSION_KEY_PLAN
|
||||||
@ -87,6 +88,7 @@ class TestSourceStage(FlowTestCase):
|
|||||||
self.assertIsNotNone(flow_token)
|
self.assertIsNotNone(flow_token)
|
||||||
session = self.client.session
|
session = self.client.session
|
||||||
plan: FlowPlan = session[SESSION_KEY_PLAN]
|
plan: FlowPlan = session[SESSION_KEY_PLAN]
|
||||||
|
plan.insert_stage(in_memory_stage(SourceStageFinal), index=0)
|
||||||
plan.context[PLAN_CONTEXT_IS_RESTORED] = flow_token
|
plan.context[PLAN_CONTEXT_IS_RESTORED] = flow_token
|
||||||
session[SESSION_KEY_PLAN] = plan
|
session[SESSION_KEY_PLAN] = plan
|
||||||
session.save()
|
session.save()
|
||||||
@ -96,4 +98,6 @@ class TestSourceStage(FlowTestCase):
|
|||||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), follow=True
|
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), follow=True
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertStageRedirects(response, reverse("authentik_core:root-redirect"))
|
self.assertStageRedirects(
|
||||||
|
response, reverse("authentik_core:if-flow", kwargs={"flow_slug": flow.slug})
|
||||||
|
)
|
||||||
|
@ -76,10 +76,10 @@ class FlowPlan:
|
|||||||
self.bindings.append(binding)
|
self.bindings.append(binding)
|
||||||
self.markers.append(marker or StageMarker())
|
self.markers.append(marker or StageMarker())
|
||||||
|
|
||||||
def insert_stage(self, stage: Stage, marker: StageMarker | None = None):
|
def insert_stage(self, stage: Stage, marker: StageMarker | None = None, index=1):
|
||||||
"""Insert stage into plan, as immediate next stage"""
|
"""Insert stage into plan, as immediate next stage"""
|
||||||
self.bindings.insert(1, FlowStageBinding(stage=stage, order=0))
|
self.bindings.insert(index, FlowStageBinding(stage=stage, order=0))
|
||||||
self.markers.insert(1, marker or StageMarker())
|
self.markers.insert(index, marker or StageMarker())
|
||||||
|
|
||||||
def redirect(self, destination: str):
|
def redirect(self, destination: str):
|
||||||
"""Insert a redirect stage as next stage"""
|
"""Insert a redirect stage as next stage"""
|
||||||
|
@ -282,16 +282,14 @@ class ConfigLoader:
|
|||||||
|
|
||||||
def get_optional_int(self, path: str, default=None) -> int | None:
|
def get_optional_int(self, path: str, default=None) -> int | None:
|
||||||
"""Wrapper for get that converts value into int or None if set"""
|
"""Wrapper for get that converts value into int or None if set"""
|
||||||
value = self.get(path, default)
|
value = self.get(path, UNSET)
|
||||||
if value is UNSET:
|
if value is UNSET:
|
||||||
return default
|
return default
|
||||||
try:
|
try:
|
||||||
return int(value)
|
return int(value)
|
||||||
except (ValueError, TypeError) as exc:
|
except (ValueError, TypeError) as exc:
|
||||||
if value is None or (isinstance(value, str) and value.lower() == "null"):
|
if value is None or (isinstance(value, str) and value.lower() == "null"):
|
||||||
return default
|
return None
|
||||||
if value is UNSET:
|
|
||||||
return default
|
|
||||||
self.log("warning", "Failed to parse config as int", path=path, exc=str(exc))
|
self.log("warning", "Failed to parse config as int", path=path, exc=str(exc))
|
||||||
return default
|
return default
|
||||||
|
|
||||||
@ -372,9 +370,9 @@ def django_db_config(config: ConfigLoader | None = None) -> dict:
|
|||||||
"sslcert": config.get("postgresql.sslcert"),
|
"sslcert": config.get("postgresql.sslcert"),
|
||||||
"sslkey": config.get("postgresql.sslkey"),
|
"sslkey": config.get("postgresql.sslkey"),
|
||||||
},
|
},
|
||||||
"CONN_MAX_AGE": CONFIG.get_optional_int("postgresql.conn_max_age", 0),
|
"CONN_MAX_AGE": config.get_optional_int("postgresql.conn_max_age", 0),
|
||||||
"CONN_HEALTH_CHECKS": CONFIG.get_bool("postgresql.conn_health_checks", False),
|
"CONN_HEALTH_CHECKS": config.get_bool("postgresql.conn_health_checks", False),
|
||||||
"DISABLE_SERVER_SIDE_CURSORS": CONFIG.get_bool(
|
"DISABLE_SERVER_SIDE_CURSORS": config.get_bool(
|
||||||
"postgresql.disable_server_side_cursors", False
|
"postgresql.disable_server_side_cursors", False
|
||||||
),
|
),
|
||||||
"TEST": {
|
"TEST": {
|
||||||
@ -383,8 +381,8 @@ def django_db_config(config: ConfigLoader | None = None) -> dict:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
conn_max_age = CONFIG.get_optional_int("postgresql.conn_max_age", UNSET)
|
conn_max_age = config.get_optional_int("postgresql.conn_max_age", UNSET)
|
||||||
disable_server_side_cursors = CONFIG.get_bool("postgresql.disable_server_side_cursors", UNSET)
|
disable_server_side_cursors = config.get_bool("postgresql.disable_server_side_cursors", UNSET)
|
||||||
if config.get_bool("postgresql.use_pgpool", False):
|
if config.get_bool("postgresql.use_pgpool", False):
|
||||||
db["default"]["DISABLE_SERVER_SIDE_CURSORS"] = True
|
db["default"]["DISABLE_SERVER_SIDE_CURSORS"] = True
|
||||||
if disable_server_side_cursors is not UNSET:
|
if disable_server_side_cursors is not UNSET:
|
||||||
|
@ -158,6 +158,18 @@ class TestConfig(TestCase):
|
|||||||
test_obj = Test()
|
test_obj = Test()
|
||||||
dumps(test_obj, indent=4, cls=AttrEncoder)
|
dumps(test_obj, indent=4, cls=AttrEncoder)
|
||||||
|
|
||||||
|
def test_get_optional_int(self):
|
||||||
|
config = ConfigLoader()
|
||||||
|
self.assertEqual(config.get_optional_int("foo", 21), 21)
|
||||||
|
self.assertEqual(config.get_optional_int("foo"), None)
|
||||||
|
config.set("foo", "21")
|
||||||
|
self.assertEqual(config.get_optional_int("foo"), 21)
|
||||||
|
self.assertEqual(config.get_optional_int("foo", 0), 21)
|
||||||
|
self.assertEqual(config.get_optional_int("foo", "null"), 21)
|
||||||
|
config.set("foo", "null")
|
||||||
|
self.assertEqual(config.get_optional_int("foo"), None)
|
||||||
|
self.assertEqual(config.get_optional_int("foo", 21), None)
|
||||||
|
|
||||||
@mock.patch.dict(environ, check_deprecations_env_vars)
|
@mock.patch.dict(environ, check_deprecations_env_vars)
|
||||||
def test_check_deprecations(self):
|
def test_check_deprecations(self):
|
||||||
"""Test config key re-write for deprecated env vars"""
|
"""Test config key re-write for deprecated env vars"""
|
||||||
@ -221,6 +233,16 @@ class TestConfig(TestCase):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_db_conn_max_age(self):
|
||||||
|
"""Test DB conn_max_age Config"""
|
||||||
|
config = ConfigLoader()
|
||||||
|
config.set("postgresql.conn_max_age", "null")
|
||||||
|
conf = django_db_config(config)
|
||||||
|
self.assertEqual(
|
||||||
|
conf["default"]["CONN_MAX_AGE"],
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
def test_db_read_replicas(self):
|
def test_db_read_replicas(self):
|
||||||
"""Test read replicas"""
|
"""Test read replicas"""
|
||||||
config = ConfigLoader()
|
config = ConfigLoader()
|
||||||
|
@ -19,7 +19,6 @@ from authentik.core.api.used_by import UsedByMixin
|
|||||||
from authentik.core.api.utils import JSONDictField, ModelSerializer, PassiveSerializer
|
from authentik.core.api.utils import JSONDictField, ModelSerializer, PassiveSerializer
|
||||||
from authentik.core.models import Provider
|
from authentik.core.models import Provider
|
||||||
from authentik.enterprise.license import LicenseKey
|
from authentik.enterprise.license import LicenseKey
|
||||||
from authentik.enterprise.providers.rac.models import RACProvider
|
|
||||||
from authentik.lib.utils.time import timedelta_from_string, timedelta_string_validator
|
from authentik.lib.utils.time import timedelta_from_string, timedelta_string_validator
|
||||||
from authentik.outposts.api.service_connections import ServiceConnectionSerializer
|
from authentik.outposts.api.service_connections import ServiceConnectionSerializer
|
||||||
from authentik.outposts.apps import MANAGED_OUTPOST, MANAGED_OUTPOST_NAME
|
from authentik.outposts.apps import MANAGED_OUTPOST, MANAGED_OUTPOST_NAME
|
||||||
@ -31,6 +30,7 @@ from authentik.outposts.models import (
|
|||||||
)
|
)
|
||||||
from authentik.providers.ldap.models import LDAPProvider
|
from authentik.providers.ldap.models import LDAPProvider
|
||||||
from authentik.providers.proxy.models import ProxyProvider
|
from authentik.providers.proxy.models import ProxyProvider
|
||||||
|
from authentik.providers.rac.models import RACProvider
|
||||||
from authentik.providers.radius.models import RadiusProvider
|
from authentik.providers.radius.models import RadiusProvider
|
||||||
|
|
||||||
|
|
||||||
|
@ -18,8 +18,6 @@ from kubernetes.config.kube_config import KUBE_CONFIG_DEFAULT_LOCATION
|
|||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
from yaml import safe_load
|
from yaml import safe_load
|
||||||
|
|
||||||
from authentik.enterprise.providers.rac.controllers.docker import RACDockerController
|
|
||||||
from authentik.enterprise.providers.rac.controllers.kubernetes import RACKubernetesController
|
|
||||||
from authentik.events.models import TaskStatus
|
from authentik.events.models import TaskStatus
|
||||||
from authentik.events.system_tasks import SystemTask, prefill_task
|
from authentik.events.system_tasks import SystemTask, prefill_task
|
||||||
from authentik.lib.config import CONFIG
|
from authentik.lib.config import CONFIG
|
||||||
@ -41,6 +39,8 @@ from authentik.providers.ldap.controllers.docker import LDAPDockerController
|
|||||||
from authentik.providers.ldap.controllers.kubernetes import LDAPKubernetesController
|
from authentik.providers.ldap.controllers.kubernetes import LDAPKubernetesController
|
||||||
from authentik.providers.proxy.controllers.docker import ProxyDockerController
|
from authentik.providers.proxy.controllers.docker import ProxyDockerController
|
||||||
from authentik.providers.proxy.controllers.kubernetes import ProxyKubernetesController
|
from authentik.providers.proxy.controllers.kubernetes import ProxyKubernetesController
|
||||||
|
from authentik.providers.rac.controllers.docker import RACDockerController
|
||||||
|
from authentik.providers.rac.controllers.kubernetes import RACKubernetesController
|
||||||
from authentik.providers.radius.controllers.docker import RadiusDockerController
|
from authentik.providers.radius.controllers.docker import RadiusDockerController
|
||||||
from authentik.providers.radius.controllers.kubernetes import RadiusKubernetesController
|
from authentik.providers.radius.controllers.kubernetes import RadiusKubernetesController
|
||||||
from authentik.root.celery import CELERY_APP
|
from authentik.root.celery import CELERY_APP
|
||||||
|
@ -128,7 +128,7 @@ class GeoIPPolicy(Policy):
|
|||||||
(geoip_data["lat"], geoip_data["long"]),
|
(geoip_data["lat"], geoip_data["long"]),
|
||||||
)
|
)
|
||||||
if self.check_history_distance and dist.km >= (
|
if self.check_history_distance and dist.km >= (
|
||||||
self.history_max_distance_km - self.distance_tolerance_km
|
self.history_max_distance_km + self.distance_tolerance_km
|
||||||
):
|
):
|
||||||
return PolicyResult(
|
return PolicyResult(
|
||||||
False, _("Distance from previous authentication is larger than threshold.")
|
False, _("Distance from previous authentication is larger than threshold.")
|
||||||
@ -139,7 +139,7 @@ class GeoIPPolicy(Policy):
|
|||||||
# clamped to be at least 1 hour
|
# clamped to be at least 1 hour
|
||||||
rel_time_hours = max(int((_now - previous_login.created).total_seconds() / 3600), 1)
|
rel_time_hours = max(int((_now - previous_login.created).total_seconds() / 3600), 1)
|
||||||
if self.check_impossible_travel and dist.km >= (
|
if self.check_impossible_travel and dist.km >= (
|
||||||
(MAX_DISTANCE_HOUR_KM * rel_time_hours) - self.distance_tolerance_km
|
(MAX_DISTANCE_HOUR_KM * rel_time_hours) + self.distance_tolerance_km
|
||||||
):
|
):
|
||||||
return PolicyResult(False, _("Distance is further than possible."))
|
return PolicyResult(False, _("Distance is further than possible."))
|
||||||
return PolicyResult(True)
|
return PolicyResult(True)
|
||||||
|
@ -148,10 +148,10 @@ class PasswordPolicy(Policy):
|
|||||||
user_inputs.append(request.user.email)
|
user_inputs.append(request.user.email)
|
||||||
if request.http_request:
|
if request.http_request:
|
||||||
user_inputs.append(request.http_request.brand.branding_title)
|
user_inputs.append(request.http_request.brand.branding_title)
|
||||||
# Only calculate result for the first 100 characters, as with over 100 char
|
# Only calculate result for the first 72 characters, as with over 100 char
|
||||||
# long passwords we can be reasonably sure that they'll surpass the score anyways
|
# long passwords we can be reasonably sure that they'll surpass the score anyways
|
||||||
# See https://github.com/dropbox/zxcvbn#runtime-latency
|
# See https://github.com/dropbox/zxcvbn#runtime-latency
|
||||||
results = zxcvbn(password[:100], user_inputs)
|
results = zxcvbn(password[:72], user_inputs)
|
||||||
LOGGER.debug("password failed", check="zxcvbn", score=results["score"])
|
LOGGER.debug("password failed", check="zxcvbn", score=results["score"])
|
||||||
result = PolicyResult(results["score"] > self.zxcvbn_score_threshold)
|
result = PolicyResult(results["score"] > self.zxcvbn_score_threshold)
|
||||||
if not result.passing:
|
if not result.passing:
|
||||||
|
@ -71,7 +71,7 @@ class CodeValidatorView(PolicyAccessView):
|
|||||||
except FlowNonApplicableException:
|
except FlowNonApplicableException:
|
||||||
LOGGER.warning("Flow not applicable to user")
|
LOGGER.warning("Flow not applicable to user")
|
||||||
return None
|
return None
|
||||||
plan.insert_stage(in_memory_stage(OAuthDeviceCodeFinishStage))
|
plan.append_stage(in_memory_stage(OAuthDeviceCodeFinishStage))
|
||||||
return plan.to_redirect(self.request, self.token.provider.authorization_flow)
|
return plan.to_redirect(self.request, self.token.provider.authorization_flow)
|
||||||
|
|
||||||
|
|
||||||
|
@ -34,5 +34,5 @@ class EndSessionView(PolicyAccessView):
|
|||||||
PLAN_CONTEXT_APPLICATION: self.application,
|
PLAN_CONTEXT_APPLICATION: self.application,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
plan.insert_stage(in_memory_stage(SessionEndStage))
|
plan.append_stage(in_memory_stage(SessionEndStage))
|
||||||
return plan.to_redirect(self.request, self.flow)
|
return plan.to_redirect(self.request, self.flow)
|
||||||
|
@ -36,17 +36,17 @@ class IngressReconciler(KubernetesObjectReconciler[V1Ingress]):
|
|||||||
def reconciler_name() -> str:
|
def reconciler_name() -> str:
|
||||||
return "ingress"
|
return "ingress"
|
||||||
|
|
||||||
def _check_annotations(self, reference: V1Ingress):
|
def _check_annotations(self, current: V1Ingress, reference: V1Ingress):
|
||||||
"""Check that all annotations *we* set are correct"""
|
"""Check that all annotations *we* set are correct"""
|
||||||
for key, value in self.get_ingress_annotations().items():
|
for key, value in reference.metadata.annotations.items():
|
||||||
if key not in reference.metadata.annotations:
|
if key not in current.metadata.annotations:
|
||||||
raise NeedsUpdate()
|
raise NeedsUpdate()
|
||||||
if reference.metadata.annotations[key] != value:
|
if current.metadata.annotations[key] != value:
|
||||||
raise NeedsUpdate()
|
raise NeedsUpdate()
|
||||||
|
|
||||||
def reconcile(self, current: V1Ingress, reference: V1Ingress):
|
def reconcile(self, current: V1Ingress, reference: V1Ingress):
|
||||||
super().reconcile(current, reference)
|
super().reconcile(current, reference)
|
||||||
self._check_annotations(reference)
|
self._check_annotations(current, reference)
|
||||||
# Create a list of all expected host and tls hosts
|
# Create a list of all expected host and tls hosts
|
||||||
expected_hosts = []
|
expected_hosts = []
|
||||||
expected_hosts_tls = []
|
expected_hosts_tls = []
|
||||||
|
@ -6,13 +6,12 @@ from rest_framework.viewsets import GenericViewSet
|
|||||||
from authentik.core.api.groups import GroupMemberSerializer
|
from authentik.core.api.groups import GroupMemberSerializer
|
||||||
from authentik.core.api.used_by import UsedByMixin
|
from authentik.core.api.used_by import UsedByMixin
|
||||||
from authentik.core.api.utils import ModelSerializer
|
from authentik.core.api.utils import ModelSerializer
|
||||||
from authentik.enterprise.api import EnterpriseRequiredMixin
|
from authentik.providers.rac.api.endpoints import EndpointSerializer
|
||||||
from authentik.enterprise.providers.rac.api.endpoints import EndpointSerializer
|
from authentik.providers.rac.api.providers import RACProviderSerializer
|
||||||
from authentik.enterprise.providers.rac.api.providers import RACProviderSerializer
|
from authentik.providers.rac.models import ConnectionToken
|
||||||
from authentik.enterprise.providers.rac.models import ConnectionToken
|
|
||||||
|
|
||||||
|
|
||||||
class ConnectionTokenSerializer(EnterpriseRequiredMixin, ModelSerializer):
|
class ConnectionTokenSerializer(ModelSerializer):
|
||||||
"""ConnectionToken Serializer"""
|
"""ConnectionToken Serializer"""
|
||||||
|
|
||||||
provider_obj = RACProviderSerializer(source="provider", read_only=True)
|
provider_obj = RACProviderSerializer(source="provider", read_only=True)
|
@ -14,10 +14,9 @@ from structlog.stdlib import get_logger
|
|||||||
from authentik.core.api.used_by import UsedByMixin
|
from authentik.core.api.used_by import UsedByMixin
|
||||||
from authentik.core.api.utils import ModelSerializer
|
from authentik.core.api.utils import ModelSerializer
|
||||||
from authentik.core.models import Provider
|
from authentik.core.models import Provider
|
||||||
from authentik.enterprise.api import EnterpriseRequiredMixin
|
|
||||||
from authentik.enterprise.providers.rac.api.providers import RACProviderSerializer
|
|
||||||
from authentik.enterprise.providers.rac.models import Endpoint
|
|
||||||
from authentik.policies.engine import PolicyEngine
|
from authentik.policies.engine import PolicyEngine
|
||||||
|
from authentik.providers.rac.api.providers import RACProviderSerializer
|
||||||
|
from authentik.providers.rac.models import Endpoint
|
||||||
from authentik.rbac.filters import ObjectFilter
|
from authentik.rbac.filters import ObjectFilter
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
@ -28,7 +27,7 @@ def user_endpoint_cache_key(user_pk: str) -> str:
|
|||||||
return f"goauthentik.io/providers/rac/endpoint_access/{user_pk}"
|
return f"goauthentik.io/providers/rac/endpoint_access/{user_pk}"
|
||||||
|
|
||||||
|
|
||||||
class EndpointSerializer(EnterpriseRequiredMixin, ModelSerializer):
|
class EndpointSerializer(ModelSerializer):
|
||||||
"""Endpoint Serializer"""
|
"""Endpoint Serializer"""
|
||||||
|
|
||||||
provider_obj = RACProviderSerializer(source="provider", read_only=True)
|
provider_obj = RACProviderSerializer(source="provider", read_only=True)
|
@ -10,7 +10,7 @@ from rest_framework.viewsets import ModelViewSet
|
|||||||
from authentik.core.api.property_mappings import PropertyMappingSerializer
|
from authentik.core.api.property_mappings import PropertyMappingSerializer
|
||||||
from authentik.core.api.used_by import UsedByMixin
|
from authentik.core.api.used_by import UsedByMixin
|
||||||
from authentik.core.api.utils import JSONDictField
|
from authentik.core.api.utils import JSONDictField
|
||||||
from authentik.enterprise.providers.rac.models import RACPropertyMapping
|
from authentik.providers.rac.models import RACPropertyMapping
|
||||||
|
|
||||||
|
|
||||||
class RACPropertyMappingSerializer(PropertyMappingSerializer):
|
class RACPropertyMappingSerializer(PropertyMappingSerializer):
|
@ -5,11 +5,10 @@ from rest_framework.viewsets import ModelViewSet
|
|||||||
|
|
||||||
from authentik.core.api.providers import ProviderSerializer
|
from authentik.core.api.providers import ProviderSerializer
|
||||||
from authentik.core.api.used_by import UsedByMixin
|
from authentik.core.api.used_by import UsedByMixin
|
||||||
from authentik.enterprise.api import EnterpriseRequiredMixin
|
from authentik.providers.rac.models import RACProvider
|
||||||
from authentik.enterprise.providers.rac.models import RACProvider
|
|
||||||
|
|
||||||
|
|
||||||
class RACProviderSerializer(EnterpriseRequiredMixin, ProviderSerializer):
|
class RACProviderSerializer(ProviderSerializer):
|
||||||
"""RACProvider Serializer"""
|
"""RACProvider Serializer"""
|
||||||
|
|
||||||
outpost_set = ListField(child=CharField(), read_only=True, source="outpost_set.all")
|
outpost_set = ListField(child=CharField(), read_only=True, source="outpost_set.all")
|
14
authentik/providers/rac/apps.py
Normal file
14
authentik/providers/rac/apps.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
"""RAC app config"""
|
||||||
|
|
||||||
|
from authentik.blueprints.apps import ManagedAppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class AuthentikProviderRAC(ManagedAppConfig):
|
||||||
|
"""authentik rac app config"""
|
||||||
|
|
||||||
|
name = "authentik.providers.rac"
|
||||||
|
label = "authentik_providers_rac"
|
||||||
|
verbose_name = "authentik Providers.RAC"
|
||||||
|
default = True
|
||||||
|
mountpoint = ""
|
||||||
|
ws_mountpoint = "authentik.providers.rac.urls"
|
@ -7,22 +7,22 @@ from channels.generic.websocket import AsyncWebsocketConsumer
|
|||||||
from django.http.request import QueryDict
|
from django.http.request import QueryDict
|
||||||
from structlog.stdlib import BoundLogger, get_logger
|
from structlog.stdlib import BoundLogger, get_logger
|
||||||
|
|
||||||
from authentik.enterprise.providers.rac.models import ConnectionToken, RACProvider
|
|
||||||
from authentik.outposts.consumer import OUTPOST_GROUP_INSTANCE
|
from authentik.outposts.consumer import OUTPOST_GROUP_INSTANCE
|
||||||
from authentik.outposts.models import Outpost, OutpostState, OutpostType
|
from authentik.outposts.models import Outpost, OutpostState, OutpostType
|
||||||
|
from authentik.providers.rac.models import ConnectionToken, RACProvider
|
||||||
|
|
||||||
# Global broadcast group, which messages are sent to when the outpost connects back
|
# Global broadcast group, which messages are sent to when the outpost connects back
|
||||||
# to authentik for a specific connection
|
# to authentik for a specific connection
|
||||||
# The `RACClientConsumer` consumer adds itself to this group on connection,
|
# The `RACClientConsumer` consumer adds itself to this group on connection,
|
||||||
# and removes itself once it has been assigned a specific outpost channel
|
# and removes itself once it has been assigned a specific outpost channel
|
||||||
RAC_CLIENT_GROUP = "group_enterprise_rac_client"
|
RAC_CLIENT_GROUP = "group_rac_client"
|
||||||
# A group for all connections in a given authentik session ID
|
# A group for all connections in a given authentik session ID
|
||||||
# A disconnect message is sent to this group when the session expires/is deleted
|
# A disconnect message is sent to this group when the session expires/is deleted
|
||||||
RAC_CLIENT_GROUP_SESSION = "group_enterprise_rac_client_%(session)s"
|
RAC_CLIENT_GROUP_SESSION = "group_rac_client_%(session)s"
|
||||||
# A group for all connections with a specific token, which in almost all cases
|
# A group for all connections with a specific token, which in almost all cases
|
||||||
# is just one connection, however this is used to disconnect the connection
|
# is just one connection, however this is used to disconnect the connection
|
||||||
# when the token is deleted
|
# when the token is deleted
|
||||||
RAC_CLIENT_GROUP_TOKEN = "group_enterprise_rac_token_%(token)s" # nosec
|
RAC_CLIENT_GROUP_TOKEN = "group_rac_token_%(token)s" # nosec
|
||||||
|
|
||||||
# Step 1: Client connects to this websocket endpoint
|
# Step 1: Client connects to this websocket endpoint
|
||||||
# Step 2: We prepare all the connection args for Guac
|
# Step 2: We prepare all the connection args for Guac
|
@ -3,7 +3,7 @@
|
|||||||
from channels.exceptions import ChannelFull
|
from channels.exceptions import ChannelFull
|
||||||
from channels.generic.websocket import AsyncWebsocketConsumer
|
from channels.generic.websocket import AsyncWebsocketConsumer
|
||||||
|
|
||||||
from authentik.enterprise.providers.rac.consumer_client import RAC_CLIENT_GROUP
|
from authentik.providers.rac.consumer_client import RAC_CLIENT_GROUP
|
||||||
|
|
||||||
|
|
||||||
class RACOutpostConsumer(AsyncWebsocketConsumer):
|
class RACOutpostConsumer(AsyncWebsocketConsumer):
|
@ -74,7 +74,7 @@ class RACProvider(Provider):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def serializer(self) -> type[Serializer]:
|
def serializer(self) -> type[Serializer]:
|
||||||
from authentik.enterprise.providers.rac.api.providers import RACProviderSerializer
|
from authentik.providers.rac.api.providers import RACProviderSerializer
|
||||||
|
|
||||||
return RACProviderSerializer
|
return RACProviderSerializer
|
||||||
|
|
||||||
@ -100,7 +100,7 @@ class Endpoint(SerializerModel, PolicyBindingModel):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def serializer(self) -> type[Serializer]:
|
def serializer(self) -> type[Serializer]:
|
||||||
from authentik.enterprise.providers.rac.api.endpoints import EndpointSerializer
|
from authentik.providers.rac.api.endpoints import EndpointSerializer
|
||||||
|
|
||||||
return EndpointSerializer
|
return EndpointSerializer
|
||||||
|
|
||||||
@ -129,7 +129,7 @@ class RACPropertyMapping(PropertyMapping):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def serializer(self) -> type[Serializer]:
|
def serializer(self) -> type[Serializer]:
|
||||||
from authentik.enterprise.providers.rac.api.property_mappings import (
|
from authentik.providers.rac.api.property_mappings import (
|
||||||
RACPropertyMappingSerializer,
|
RACPropertyMappingSerializer,
|
||||||
)
|
)
|
||||||
|
|
@ -4,18 +4,17 @@ from asgiref.sync import async_to_sync
|
|||||||
from channels.layers import get_channel_layer
|
from channels.layers import get_channel_layer
|
||||||
from django.contrib.auth.signals import user_logged_out
|
from django.contrib.auth.signals import user_logged_out
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.db.models import Model
|
from django.db.models.signals import post_delete, post_save, pre_delete
|
||||||
from django.db.models.signals import post_save, pre_delete
|
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
|
|
||||||
from authentik.core.models import User
|
from authentik.core.models import User
|
||||||
from authentik.enterprise.providers.rac.api.endpoints import user_endpoint_cache_key
|
from authentik.providers.rac.api.endpoints import user_endpoint_cache_key
|
||||||
from authentik.enterprise.providers.rac.consumer_client import (
|
from authentik.providers.rac.consumer_client import (
|
||||||
RAC_CLIENT_GROUP_SESSION,
|
RAC_CLIENT_GROUP_SESSION,
|
||||||
RAC_CLIENT_GROUP_TOKEN,
|
RAC_CLIENT_GROUP_TOKEN,
|
||||||
)
|
)
|
||||||
from authentik.enterprise.providers.rac.models import ConnectionToken, Endpoint
|
from authentik.providers.rac.models import ConnectionToken, Endpoint
|
||||||
|
|
||||||
|
|
||||||
@receiver(user_logged_out)
|
@receiver(user_logged_out)
|
||||||
@ -46,12 +45,8 @@ def pre_delete_connection_token_disconnect(sender, instance: ConnectionToken, **
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_save, sender=Endpoint)
|
@receiver([post_save, post_delete], sender=Endpoint)
|
||||||
def post_save_endpoint(sender: type[Model], instance, created: bool, **_):
|
def post_save_post_delete_endpoint(**_):
|
||||||
"""Clear user's endpoint cache upon endpoint creation"""
|
"""Clear user's endpoint cache upon endpoint creation or deletion"""
|
||||||
if not created: # pragma: no cover
|
|
||||||
return
|
|
||||||
|
|
||||||
# Delete user endpoint cache
|
|
||||||
keys = cache.keys(user_endpoint_cache_key("*"))
|
keys = cache.keys(user_endpoint_cache_key("*"))
|
||||||
cache.delete_many(keys)
|
cache.delete_many(keys)
|
@ -3,7 +3,7 @@
|
|||||||
{% load authentik_core %}
|
{% load authentik_core %}
|
||||||
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
<script src="{% versioned_script 'dist/enterprise/rac/index-%v.js' %}" type="module"></script>
|
<script src="{% versioned_script 'dist/rac/index-%v.js' %}" type="module"></script>
|
||||||
<meta name="theme-color" content="#18191a" media="(prefers-color-scheme: dark)">
|
<meta name="theme-color" content="#18191a" media="(prefers-color-scheme: dark)">
|
||||||
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">
|
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">
|
||||||
<link rel="icon" href="{{ tenant.branding_favicon_url }}">
|
<link rel="icon" href="{{ tenant.branding_favicon_url }}">
|
@ -1,16 +1,9 @@
|
|||||||
"""Test RAC Provider"""
|
"""Test RAC Provider"""
|
||||||
|
|
||||||
from datetime import timedelta
|
|
||||||
from time import mktime
|
|
||||||
from unittest.mock import MagicMock, patch
|
|
||||||
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.timezone import now
|
|
||||||
from rest_framework.test import APITestCase
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
|
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
|
||||||
from authentik.enterprise.license import LicenseKey
|
|
||||||
from authentik.enterprise.models import License
|
|
||||||
from authentik.lib.generators import generate_id
|
from authentik.lib.generators import generate_id
|
||||||
|
|
||||||
|
|
||||||
@ -20,21 +13,8 @@ class TestAPI(APITestCase):
|
|||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
self.user = create_test_admin_user()
|
self.user = create_test_admin_user()
|
||||||
|
|
||||||
@patch(
|
|
||||||
"authentik.enterprise.license.LicenseKey.validate",
|
|
||||||
MagicMock(
|
|
||||||
return_value=LicenseKey(
|
|
||||||
aud="",
|
|
||||||
exp=int(mktime((now() + timedelta(days=3000)).timetuple())),
|
|
||||||
name=generate_id(),
|
|
||||||
internal_users=100,
|
|
||||||
external_users=100,
|
|
||||||
)
|
|
||||||
),
|
|
||||||
)
|
|
||||||
def test_create(self):
|
def test_create(self):
|
||||||
"""Test creation of RAC Provider"""
|
"""Test creation of RAC Provider"""
|
||||||
License.objects.create(key=generate_id())
|
|
||||||
self.client.force_login(self.user)
|
self.client.force_login(self.user)
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse("authentik_api:racprovider-list"),
|
reverse("authentik_api:racprovider-list"),
|
@ -5,10 +5,10 @@ from rest_framework.test import APITestCase
|
|||||||
|
|
||||||
from authentik.core.models import Application
|
from authentik.core.models import Application
|
||||||
from authentik.core.tests.utils import create_test_admin_user
|
from authentik.core.tests.utils import create_test_admin_user
|
||||||
from authentik.enterprise.providers.rac.models import Endpoint, Protocols, RACProvider
|
|
||||||
from authentik.lib.generators import generate_id
|
from authentik.lib.generators import generate_id
|
||||||
from authentik.policies.dummy.models import DummyPolicy
|
from authentik.policies.dummy.models import DummyPolicy
|
||||||
from authentik.policies.models import PolicyBinding
|
from authentik.policies.models import PolicyBinding
|
||||||
|
from authentik.providers.rac.models import Endpoint, Protocols, RACProvider
|
||||||
|
|
||||||
|
|
||||||
class TestEndpointsAPI(APITestCase):
|
class TestEndpointsAPI(APITestCase):
|
@ -4,14 +4,14 @@ from django.test import TransactionTestCase
|
|||||||
|
|
||||||
from authentik.core.models import Application, AuthenticatedSession
|
from authentik.core.models import Application, AuthenticatedSession
|
||||||
from authentik.core.tests.utils import create_test_admin_user
|
from authentik.core.tests.utils import create_test_admin_user
|
||||||
from authentik.enterprise.providers.rac.models import (
|
from authentik.lib.generators import generate_id
|
||||||
|
from authentik.providers.rac.models import (
|
||||||
ConnectionToken,
|
ConnectionToken,
|
||||||
Endpoint,
|
Endpoint,
|
||||||
Protocols,
|
Protocols,
|
||||||
RACPropertyMapping,
|
RACPropertyMapping,
|
||||||
RACProvider,
|
RACProvider,
|
||||||
)
|
)
|
||||||
from authentik.lib.generators import generate_id
|
|
||||||
|
|
||||||
|
|
||||||
class TestModels(TransactionTestCase):
|
class TestModels(TransactionTestCase):
|
@ -1,23 +1,17 @@
|
|||||||
"""RAC Views tests"""
|
"""RAC Views tests"""
|
||||||
|
|
||||||
from datetime import timedelta
|
|
||||||
from json import loads
|
from json import loads
|
||||||
from time import mktime
|
|
||||||
from unittest.mock import MagicMock, patch
|
|
||||||
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.timezone import now
|
|
||||||
from rest_framework.test import APITestCase
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
from authentik.core.models import Application
|
from authentik.core.models import Application
|
||||||
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
|
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
|
||||||
from authentik.enterprise.license import LicenseKey
|
|
||||||
from authentik.enterprise.models import License
|
|
||||||
from authentik.enterprise.providers.rac.models import Endpoint, Protocols, RACProvider
|
|
||||||
from authentik.lib.generators import generate_id
|
from authentik.lib.generators import generate_id
|
||||||
from authentik.policies.denied import AccessDeniedResponse
|
from authentik.policies.denied import AccessDeniedResponse
|
||||||
from authentik.policies.dummy.models import DummyPolicy
|
from authentik.policies.dummy.models import DummyPolicy
|
||||||
from authentik.policies.models import PolicyBinding
|
from authentik.policies.models import PolicyBinding
|
||||||
|
from authentik.providers.rac.models import Endpoint, Protocols, RACProvider
|
||||||
|
|
||||||
|
|
||||||
class TestRACViews(APITestCase):
|
class TestRACViews(APITestCase):
|
||||||
@ -39,21 +33,8 @@ class TestRACViews(APITestCase):
|
|||||||
provider=self.provider,
|
provider=self.provider,
|
||||||
)
|
)
|
||||||
|
|
||||||
@patch(
|
|
||||||
"authentik.enterprise.license.LicenseKey.validate",
|
|
||||||
MagicMock(
|
|
||||||
return_value=LicenseKey(
|
|
||||||
aud="",
|
|
||||||
exp=int(mktime((now() + timedelta(days=3000)).timetuple())),
|
|
||||||
name=generate_id(),
|
|
||||||
internal_users=100,
|
|
||||||
external_users=100,
|
|
||||||
)
|
|
||||||
),
|
|
||||||
)
|
|
||||||
def test_no_policy(self):
|
def test_no_policy(self):
|
||||||
"""Test request"""
|
"""Test request"""
|
||||||
License.objects.create(key=generate_id())
|
|
||||||
self.client.force_login(self.user)
|
self.client.force_login(self.user)
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
reverse(
|
reverse(
|
||||||
@ -70,18 +51,6 @@ class TestRACViews(APITestCase):
|
|||||||
final_response = self.client.get(next_url)
|
final_response = self.client.get(next_url)
|
||||||
self.assertEqual(final_response.status_code, 200)
|
self.assertEqual(final_response.status_code, 200)
|
||||||
|
|
||||||
@patch(
|
|
||||||
"authentik.enterprise.license.LicenseKey.validate",
|
|
||||||
MagicMock(
|
|
||||||
return_value=LicenseKey(
|
|
||||||
aud="",
|
|
||||||
exp=int(mktime((now() + timedelta(days=3000)).timetuple())),
|
|
||||||
name=generate_id(),
|
|
||||||
internal_users=100,
|
|
||||||
external_users=100,
|
|
||||||
)
|
|
||||||
),
|
|
||||||
)
|
|
||||||
def test_app_deny(self):
|
def test_app_deny(self):
|
||||||
"""Test request (deny on app level)"""
|
"""Test request (deny on app level)"""
|
||||||
PolicyBinding.objects.create(
|
PolicyBinding.objects.create(
|
||||||
@ -89,7 +58,6 @@ class TestRACViews(APITestCase):
|
|||||||
policy=DummyPolicy.objects.create(name="deny", result=False, wait_min=1, wait_max=2),
|
policy=DummyPolicy.objects.create(name="deny", result=False, wait_min=1, wait_max=2),
|
||||||
order=0,
|
order=0,
|
||||||
)
|
)
|
||||||
License.objects.create(key=generate_id())
|
|
||||||
self.client.force_login(self.user)
|
self.client.force_login(self.user)
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
reverse(
|
reverse(
|
||||||
@ -99,18 +67,6 @@ class TestRACViews(APITestCase):
|
|||||||
)
|
)
|
||||||
self.assertIsInstance(response, AccessDeniedResponse)
|
self.assertIsInstance(response, AccessDeniedResponse)
|
||||||
|
|
||||||
@patch(
|
|
||||||
"authentik.enterprise.license.LicenseKey.validate",
|
|
||||||
MagicMock(
|
|
||||||
return_value=LicenseKey(
|
|
||||||
aud="",
|
|
||||||
exp=int(mktime((now() + timedelta(days=3000)).timetuple())),
|
|
||||||
name=generate_id(),
|
|
||||||
internal_users=100,
|
|
||||||
external_users=100,
|
|
||||||
)
|
|
||||||
),
|
|
||||||
)
|
|
||||||
def test_endpoint_deny(self):
|
def test_endpoint_deny(self):
|
||||||
"""Test request (deny on endpoint level)"""
|
"""Test request (deny on endpoint level)"""
|
||||||
PolicyBinding.objects.create(
|
PolicyBinding.objects.create(
|
||||||
@ -118,7 +74,6 @@ class TestRACViews(APITestCase):
|
|||||||
policy=DummyPolicy.objects.create(name="deny", result=False, wait_min=1, wait_max=2),
|
policy=DummyPolicy.objects.create(name="deny", result=False, wait_min=1, wait_max=2),
|
||||||
order=0,
|
order=0,
|
||||||
)
|
)
|
||||||
License.objects.create(key=generate_id())
|
|
||||||
self.client.force_login(self.user)
|
self.client.force_login(self.user)
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
reverse(
|
reverse(
|
@ -4,14 +4,14 @@ from channels.auth import AuthMiddleware
|
|||||||
from channels.sessions import CookieMiddleware
|
from channels.sessions import CookieMiddleware
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from authentik.enterprise.providers.rac.api.connection_tokens import ConnectionTokenViewSet
|
|
||||||
from authentik.enterprise.providers.rac.api.endpoints import EndpointViewSet
|
|
||||||
from authentik.enterprise.providers.rac.api.property_mappings import RACPropertyMappingViewSet
|
|
||||||
from authentik.enterprise.providers.rac.api.providers import RACProviderViewSet
|
|
||||||
from authentik.enterprise.providers.rac.consumer_client import RACClientConsumer
|
|
||||||
from authentik.enterprise.providers.rac.consumer_outpost import RACOutpostConsumer
|
|
||||||
from authentik.enterprise.providers.rac.views import RACInterface, RACStartView
|
|
||||||
from authentik.outposts.channels import TokenOutpostMiddleware
|
from authentik.outposts.channels import TokenOutpostMiddleware
|
||||||
|
from authentik.providers.rac.api.connection_tokens import ConnectionTokenViewSet
|
||||||
|
from authentik.providers.rac.api.endpoints import EndpointViewSet
|
||||||
|
from authentik.providers.rac.api.property_mappings import RACPropertyMappingViewSet
|
||||||
|
from authentik.providers.rac.api.providers import RACProviderViewSet
|
||||||
|
from authentik.providers.rac.consumer_client import RACClientConsumer
|
||||||
|
from authentik.providers.rac.consumer_outpost import RACOutpostConsumer
|
||||||
|
from authentik.providers.rac.views import RACInterface, RACStartView
|
||||||
from authentik.root.asgi_middleware import SessionMiddleware
|
from authentik.root.asgi_middleware import SessionMiddleware
|
||||||
from authentik.root.middleware import ChannelsLoggingMiddleware
|
from authentik.root.middleware import ChannelsLoggingMiddleware
|
||||||
|
|
@ -10,8 +10,6 @@ from django.utils.translation import gettext as _
|
|||||||
|
|
||||||
from authentik.core.models import Application, AuthenticatedSession
|
from authentik.core.models import Application, AuthenticatedSession
|
||||||
from authentik.core.views.interface import InterfaceView
|
from authentik.core.views.interface import InterfaceView
|
||||||
from authentik.enterprise.policy import EnterprisePolicyAccessView
|
|
||||||
from authentik.enterprise.providers.rac.models import ConnectionToken, Endpoint, RACProvider
|
|
||||||
from authentik.events.models import Event, EventAction
|
from authentik.events.models import Event, EventAction
|
||||||
from authentik.flows.challenge import RedirectChallenge
|
from authentik.flows.challenge import RedirectChallenge
|
||||||
from authentik.flows.exceptions import FlowNonApplicableException
|
from authentik.flows.exceptions import FlowNonApplicableException
|
||||||
@ -20,9 +18,11 @@ from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, FlowPlanner
|
|||||||
from authentik.flows.stage import RedirectStage
|
from authentik.flows.stage import RedirectStage
|
||||||
from authentik.lib.utils.time import timedelta_from_string
|
from authentik.lib.utils.time import timedelta_from_string
|
||||||
from authentik.policies.engine import PolicyEngine
|
from authentik.policies.engine import PolicyEngine
|
||||||
|
from authentik.policies.views import PolicyAccessView
|
||||||
|
from authentik.providers.rac.models import ConnectionToken, Endpoint, RACProvider
|
||||||
|
|
||||||
|
|
||||||
class RACStartView(EnterprisePolicyAccessView):
|
class RACStartView(PolicyAccessView):
|
||||||
"""Start a RAC connection by checking access and creating a connection token"""
|
"""Start a RAC connection by checking access and creating a connection token"""
|
||||||
|
|
||||||
endpoint: Endpoint
|
endpoint: Endpoint
|
||||||
@ -46,7 +46,7 @@ class RACStartView(EnterprisePolicyAccessView):
|
|||||||
)
|
)
|
||||||
except FlowNonApplicableException:
|
except FlowNonApplicableException:
|
||||||
raise Http404 from None
|
raise Http404 from None
|
||||||
plan.insert_stage(
|
plan.append_stage(
|
||||||
in_memory_stage(
|
in_memory_stage(
|
||||||
RACFinalStage,
|
RACFinalStage,
|
||||||
application=self.application,
|
application=self.application,
|
@ -61,7 +61,7 @@ class SAMLSLOView(PolicyAccessView):
|
|||||||
PLAN_CONTEXT_APPLICATION: self.application,
|
PLAN_CONTEXT_APPLICATION: self.application,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
plan.insert_stage(in_memory_stage(SessionEndStage))
|
plan.append_stage(in_memory_stage(SessionEndStage))
|
||||||
return plan.to_redirect(self.request, self.flow)
|
return plan.to_redirect(self.request, self.flow)
|
||||||
|
|
||||||
def post(self, request: HttpRequest, application_slug: str) -> HttpResponse:
|
def post(self, request: HttpRequest, application_slug: str) -> HttpResponse:
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
"""User client"""
|
"""User client"""
|
||||||
|
|
||||||
|
from django.db import transaction
|
||||||
|
from django.utils.http import urlencode
|
||||||
from pydantic import ValidationError
|
from pydantic import ValidationError
|
||||||
|
|
||||||
from authentik.core.models import User
|
from authentik.core.models import User
|
||||||
from authentik.lib.sync.mapper import PropertyMappingManager
|
from authentik.lib.sync.mapper import PropertyMappingManager
|
||||||
from authentik.lib.sync.outgoing.exceptions import StopSync
|
from authentik.lib.sync.outgoing.exceptions import ObjectExistsSyncException, StopSync
|
||||||
from authentik.policies.utils import delete_none_values
|
from authentik.policies.utils import delete_none_values
|
||||||
from authentik.providers.scim.clients.base import SCIMClient
|
from authentik.providers.scim.clients.base import SCIMClient
|
||||||
from authentik.providers.scim.clients.schema import SCIM_USER_SCHEMA
|
from authentik.providers.scim.clients.schema import SCIM_USER_SCHEMA
|
||||||
@ -55,18 +57,35 @@ class SCIMUserClient(SCIMClient[User, SCIMProviderUser, SCIMUserSchema]):
|
|||||||
def create(self, user: User):
|
def create(self, user: User):
|
||||||
"""Create user from scratch and create a connection object"""
|
"""Create user from scratch and create a connection object"""
|
||||||
scim_user = self.to_schema(user, None)
|
scim_user = self.to_schema(user, None)
|
||||||
response = self._request(
|
with transaction.atomic():
|
||||||
"POST",
|
try:
|
||||||
"/Users",
|
response = self._request(
|
||||||
json=scim_user.model_dump(
|
"POST",
|
||||||
mode="json",
|
"/Users",
|
||||||
exclude_unset=True,
|
json=scim_user.model_dump(
|
||||||
),
|
mode="json",
|
||||||
)
|
exclude_unset=True,
|
||||||
scim_id = response.get("id")
|
),
|
||||||
if not scim_id or scim_id == "":
|
)
|
||||||
raise StopSync("SCIM Response with missing or invalid `id`")
|
except ObjectExistsSyncException as exc:
|
||||||
return SCIMProviderUser.objects.create(provider=self.provider, user=user, scim_id=scim_id)
|
if not self._config.filter.supported:
|
||||||
|
raise exc
|
||||||
|
users = self._request(
|
||||||
|
"GET", f"/Users?{urlencode({'filter': f'userName eq {scim_user.userName}'})}"
|
||||||
|
)
|
||||||
|
users_res = users.get("Resources", [])
|
||||||
|
if len(users_res) < 1:
|
||||||
|
raise exc
|
||||||
|
return SCIMProviderUser.objects.create(
|
||||||
|
provider=self.provider, user=user, scim_id=users_res[0]["id"]
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
scim_id = response.get("id")
|
||||||
|
if not scim_id or scim_id == "":
|
||||||
|
raise StopSync("SCIM Response with missing or invalid `id`")
|
||||||
|
return SCIMProviderUser.objects.create(
|
||||||
|
provider=self.provider, user=user, scim_id=scim_id
|
||||||
|
)
|
||||||
|
|
||||||
def update(self, user: User, connection: SCIMProviderUser):
|
def update(self, user: User, connection: SCIMProviderUser):
|
||||||
"""Update existing user"""
|
"""Update existing user"""
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.contrib.auth.models import Permission
|
from django.contrib.auth.models import Permission
|
||||||
from django.db.models import Q, QuerySet
|
from django.db.models import QuerySet
|
||||||
from django_filters.filters import ModelChoiceFilter
|
from django_filters.filters import ModelChoiceFilter
|
||||||
from django_filters.filterset import FilterSet
|
from django_filters.filterset import FilterSet
|
||||||
from django_filters.rest_framework import DjangoFilterBackend
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
@ -18,7 +18,6 @@ from rest_framework.filters import OrderingFilter, SearchFilter
|
|||||||
from rest_framework.permissions import IsAuthenticated
|
from rest_framework.permissions import IsAuthenticated
|
||||||
from rest_framework.viewsets import ReadOnlyModelViewSet
|
from rest_framework.viewsets import ReadOnlyModelViewSet
|
||||||
|
|
||||||
from authentik.blueprints.v1.importer import excluded_models
|
|
||||||
from authentik.core.api.utils import ModelSerializer, PassiveSerializer
|
from authentik.core.api.utils import ModelSerializer, PassiveSerializer
|
||||||
from authentik.core.models import User
|
from authentik.core.models import User
|
||||||
from authentik.lib.validators import RequiredTogetherValidator
|
from authentik.lib.validators import RequiredTogetherValidator
|
||||||
@ -106,13 +105,13 @@ class RBACPermissionViewSet(ReadOnlyModelViewSet):
|
|||||||
]
|
]
|
||||||
|
|
||||||
def get_queryset(self) -> QuerySet:
|
def get_queryset(self) -> QuerySet:
|
||||||
query = Q()
|
return (
|
||||||
for model in excluded_models():
|
Permission.objects.all()
|
||||||
query |= Q(
|
.select_related("content_type")
|
||||||
content_type__app_label=model._meta.app_label,
|
.filter(
|
||||||
content_type__model=model._meta.model_name,
|
content_type__app_label__startswith="authentik",
|
||||||
)
|
)
|
||||||
return Permission.objects.all().select_related("content_type").exclude(query)
|
)
|
||||||
|
|
||||||
|
|
||||||
class PermissionAssignSerializer(PassiveSerializer):
|
class PermissionAssignSerializer(PassiveSerializer):
|
||||||
|
@ -87,6 +87,7 @@ TENANT_APPS = [
|
|||||||
"authentik.providers.ldap",
|
"authentik.providers.ldap",
|
||||||
"authentik.providers.oauth2",
|
"authentik.providers.oauth2",
|
||||||
"authentik.providers.proxy",
|
"authentik.providers.proxy",
|
||||||
|
"authentik.providers.rac",
|
||||||
"authentik.providers.radius",
|
"authentik.providers.radius",
|
||||||
"authentik.providers.saml",
|
"authentik.providers.saml",
|
||||||
"authentik.providers.scim",
|
"authentik.providers.scim",
|
||||||
|
@ -68,8 +68,6 @@ class OAuth2Client(BaseOAuthClient):
|
|||||||
error_desc = self.get_request_arg("error_description", None)
|
error_desc = self.get_request_arg("error_description", None)
|
||||||
return {"error": error_desc or error or _("No token received.")}
|
return {"error": error_desc or error or _("No token received.")}
|
||||||
args = {
|
args = {
|
||||||
"client_id": self.get_client_id(),
|
|
||||||
"client_secret": self.get_client_secret(),
|
|
||||||
"redirect_uri": callback,
|
"redirect_uri": callback,
|
||||||
"code": code,
|
"code": code,
|
||||||
"grant_type": "authorization_code",
|
"grant_type": "authorization_code",
|
||||||
|
@ -28,7 +28,7 @@ def update_well_known_jwks(self: SystemTask):
|
|||||||
LOGGER.warning("Failed to update well_known", source=source, exc=exc, text=text)
|
LOGGER.warning("Failed to update well_known", source=source, exc=exc, text=text)
|
||||||
messages.append(f"Failed to update OIDC configuration for {source.slug}")
|
messages.append(f"Failed to update OIDC configuration for {source.slug}")
|
||||||
continue
|
continue
|
||||||
config = well_known_config.json()
|
config: dict = well_known_config.json()
|
||||||
try:
|
try:
|
||||||
dirty = False
|
dirty = False
|
||||||
source_attr_key = (
|
source_attr_key = (
|
||||||
@ -40,7 +40,9 @@ def update_well_known_jwks(self: SystemTask):
|
|||||||
for source_attr, config_key in source_attr_key:
|
for source_attr, config_key in source_attr_key:
|
||||||
# Check if we're actually changing anything to only
|
# Check if we're actually changing anything to only
|
||||||
# save when something has changed
|
# save when something has changed
|
||||||
if getattr(source, source_attr, "") != config[config_key]:
|
if config_key not in config:
|
||||||
|
continue
|
||||||
|
if getattr(source, source_attr, "") != config.get(config_key, ""):
|
||||||
dirty = True
|
dirty = True
|
||||||
setattr(source, source_attr, config[config_key])
|
setattr(source, source_attr, config[config_key])
|
||||||
except (IndexError, KeyError) as exc:
|
except (IndexError, KeyError) as exc:
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from requests import RequestException
|
||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
from authentik.sources.oauth.clients.oauth2 import UserprofileHeaderAuthClient
|
from authentik.sources.oauth.clients.oauth2 import UserprofileHeaderAuthClient
|
||||||
@ -21,10 +22,35 @@ class AzureADOAuthRedirect(OAuthRedirect):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class AzureADClient(UserprofileHeaderAuthClient):
|
||||||
|
"""Fetch AzureAD group information"""
|
||||||
|
|
||||||
|
def get_profile_info(self, token):
|
||||||
|
profile_data = super().get_profile_info(token)
|
||||||
|
if "https://graph.microsoft.com/GroupMember.Read.All" not in self.source.additional_scopes:
|
||||||
|
return profile_data
|
||||||
|
group_response = self.session.request(
|
||||||
|
"get",
|
||||||
|
"https://graph.microsoft.com/v1.0/me/memberOf",
|
||||||
|
headers={"Authorization": f"{token['token_type']} {token['access_token']}"},
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
group_response.raise_for_status()
|
||||||
|
except RequestException as exc:
|
||||||
|
LOGGER.warning(
|
||||||
|
"Unable to fetch user profile",
|
||||||
|
exc=exc,
|
||||||
|
response=exc.response.text if exc.response else str(exc),
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
profile_data["raw_groups"] = group_response.json()
|
||||||
|
return profile_data
|
||||||
|
|
||||||
|
|
||||||
class AzureADOAuthCallback(OpenIDConnectOAuth2Callback):
|
class AzureADOAuthCallback(OpenIDConnectOAuth2Callback):
|
||||||
"""AzureAD OAuth2 Callback"""
|
"""AzureAD OAuth2 Callback"""
|
||||||
|
|
||||||
client_class = UserprofileHeaderAuthClient
|
client_class = AzureADClient
|
||||||
|
|
||||||
def get_user_id(self, info: dict[str, str]) -> str:
|
def get_user_id(self, info: dict[str, str]) -> str:
|
||||||
# Default try to get `id` for the Graph API endpoint
|
# Default try to get `id` for the Graph API endpoint
|
||||||
@ -53,8 +79,24 @@ class AzureADType(SourceType):
|
|||||||
|
|
||||||
def get_base_user_properties(self, info: dict[str, Any], **kwargs) -> dict[str, Any]:
|
def get_base_user_properties(self, info: dict[str, Any], **kwargs) -> dict[str, Any]:
|
||||||
mail = info.get("mail", None) or info.get("otherMails", [None])[0]
|
mail = info.get("mail", None) or info.get("otherMails", [None])[0]
|
||||||
|
# Format group info
|
||||||
|
groups = []
|
||||||
|
group_id_dict = {}
|
||||||
|
for group in info.get("raw_groups", {}).get("value", []):
|
||||||
|
if group["@odata.type"] != "#microsoft.graph.group":
|
||||||
|
continue
|
||||||
|
groups.append(group["id"])
|
||||||
|
group_id_dict[group["id"]] = group
|
||||||
|
info["raw_groups"] = group_id_dict
|
||||||
return {
|
return {
|
||||||
"username": info.get("userPrincipalName"),
|
"username": info.get("userPrincipalName"),
|
||||||
"email": mail,
|
"email": mail,
|
||||||
"name": info.get("displayName"),
|
"name": info.get("displayName"),
|
||||||
|
"groups": groups,
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_base_group_properties(self, source, group_id, **kwargs):
|
||||||
|
raw_group = kwargs["info"]["raw_groups"][group_id]
|
||||||
|
return {
|
||||||
|
"name": raw_group["displayName"],
|
||||||
}
|
}
|
||||||
|
@ -33,6 +33,7 @@ from authentik.flows.planner import (
|
|||||||
)
|
)
|
||||||
from authentik.flows.stage import ChallengeStageView
|
from authentik.flows.stage import ChallengeStageView
|
||||||
from authentik.flows.views.executor import NEXT_ARG_NAME, SESSION_KEY_GET, SESSION_KEY_PLAN
|
from authentik.flows.views.executor import NEXT_ARG_NAME, SESSION_KEY_GET, SESSION_KEY_PLAN
|
||||||
|
from authentik.lib.utils.urls import is_url_absolute
|
||||||
from authentik.lib.views import bad_request_message
|
from authentik.lib.views import bad_request_message
|
||||||
from authentik.providers.saml.utils.encoding import nice64
|
from authentik.providers.saml.utils.encoding import nice64
|
||||||
from authentik.sources.saml.exceptions import MissingSAMLResponse, UnsupportedNameIDFormat
|
from authentik.sources.saml.exceptions import MissingSAMLResponse, UnsupportedNameIDFormat
|
||||||
@ -73,6 +74,8 @@ class InitiateView(View):
|
|||||||
final_redirect = self.request.session.get(SESSION_KEY_GET, {}).get(
|
final_redirect = self.request.session.get(SESSION_KEY_GET, {}).get(
|
||||||
NEXT_ARG_NAME, "authentik_core:if-user"
|
NEXT_ARG_NAME, "authentik_core:if-user"
|
||||||
)
|
)
|
||||||
|
if not is_url_absolute(final_redirect):
|
||||||
|
final_redirect = "authentik_core:if-user"
|
||||||
kwargs.update(
|
kwargs.update(
|
||||||
{
|
{
|
||||||
PLAN_CONTEXT_SSO: True,
|
PLAN_CONTEXT_SSO: True,
|
||||||
|
@ -7,6 +7,7 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
from django.views import View
|
from django.views import View
|
||||||
from rest_framework.serializers import BaseSerializer
|
from rest_framework.serializers import BaseSerializer
|
||||||
|
|
||||||
|
from authentik.core.types import UserSettingSerializer
|
||||||
from authentik.events.models import Event, EventAction
|
from authentik.events.models import Event, EventAction
|
||||||
from authentik.flows.exceptions import StageInvalidException
|
from authentik.flows.exceptions import StageInvalidException
|
||||||
from authentik.flows.models import ConfigurableStage, FriendlyNamedStage, Stage
|
from authentik.flows.models import ConfigurableStage, FriendlyNamedStage, Stage
|
||||||
@ -71,6 +72,14 @@ class AuthenticatorEmailStage(ConfigurableStage, FriendlyNamedStage, Stage):
|
|||||||
def component(self) -> str:
|
def component(self) -> str:
|
||||||
return "ak-stage-authenticator-email-form"
|
return "ak-stage-authenticator-email-form"
|
||||||
|
|
||||||
|
def ui_user_settings(self) -> UserSettingSerializer | None:
|
||||||
|
return UserSettingSerializer(
|
||||||
|
data={
|
||||||
|
"title": self.friendly_name or str(self._meta.verbose_name),
|
||||||
|
"component": "ak-user-settings-authenticator-email",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def backend_class(self) -> type[BaseEmailBackend]:
|
def backend_class(self) -> type[BaseEmailBackend]:
|
||||||
"""Get the email backend class to use"""
|
"""Get the email backend class to use"""
|
||||||
|
@ -300,9 +300,11 @@ class TestAuthenticatorEmailStage(FlowTestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertTrue(device.confirmed)
|
self.assertTrue(device.confirmed)
|
||||||
# Session key should be removed after device is saved
|
# Get a fresh session to check if the key was removed
|
||||||
device.save()
|
session = self.client.session
|
||||||
self.assertNotIn(SESSION_KEY_EMAIL_DEVICE, self.client.session)
|
session.save()
|
||||||
|
session.load()
|
||||||
|
self.assertNotIn(SESSION_KEY_EMAIL_DEVICE, session)
|
||||||
|
|
||||||
def test_model_properties_and_methods(self):
|
def test_model_properties_and_methods(self):
|
||||||
"""Test model properties"""
|
"""Test model properties"""
|
||||||
|
@ -12,6 +12,7 @@ from structlog.stdlib import get_logger
|
|||||||
|
|
||||||
from authentik.events.models import Event, EventAction, TaskStatus
|
from authentik.events.models import Event, EventAction, TaskStatus
|
||||||
from authentik.events.system_tasks import SystemTask
|
from authentik.events.system_tasks import SystemTask
|
||||||
|
from authentik.lib.utils.reflection import class_to_path, path_to_class
|
||||||
from authentik.root.celery import CELERY_APP
|
from authentik.root.celery import CELERY_APP
|
||||||
from authentik.stages.authenticator_email.models import AuthenticatorEmailStage
|
from authentik.stages.authenticator_email.models import AuthenticatorEmailStage
|
||||||
from authentik.stages.email.models import EmailStage
|
from authentik.stages.email.models import EmailStage
|
||||||
@ -32,9 +33,10 @@ def send_mails(
|
|||||||
Celery group promise for the email sending tasks
|
Celery group promise for the email sending tasks
|
||||||
"""
|
"""
|
||||||
tasks = []
|
tasks = []
|
||||||
stage_class = stage.__class__
|
# Use the class path instead of the class itself for serialization
|
||||||
|
stage_class_path = class_to_path(stage.__class__)
|
||||||
for message in messages:
|
for message in messages:
|
||||||
tasks.append(send_mail.s(message.__dict__, stage_class, str(stage.pk)))
|
tasks.append(send_mail.s(message.__dict__, stage_class_path, str(stage.pk)))
|
||||||
lazy_group = group(*tasks)
|
lazy_group = group(*tasks)
|
||||||
promise = lazy_group()
|
promise = lazy_group()
|
||||||
return promise
|
return promise
|
||||||
@ -61,7 +63,7 @@ def get_email_body(email: EmailMultiAlternatives) -> str:
|
|||||||
def send_mail(
|
def send_mail(
|
||||||
self: SystemTask,
|
self: SystemTask,
|
||||||
message: dict[Any, Any],
|
message: dict[Any, Any],
|
||||||
stage_class: EmailStage | AuthenticatorEmailStage = EmailStage,
|
stage_class_path: str | None = None,
|
||||||
email_stage_pk: str | None = None,
|
email_stage_pk: str | None = None,
|
||||||
):
|
):
|
||||||
"""Send Email for Email Stage. Retries are scheduled automatically."""
|
"""Send Email for Email Stage. Retries are scheduled automatically."""
|
||||||
@ -69,9 +71,10 @@ def send_mail(
|
|||||||
message_id = make_msgid(domain=DNS_NAME)
|
message_id = make_msgid(domain=DNS_NAME)
|
||||||
self.set_uid(slugify(message_id.replace(".", "_").replace("@", "_")))
|
self.set_uid(slugify(message_id.replace(".", "_").replace("@", "_")))
|
||||||
try:
|
try:
|
||||||
if not email_stage_pk:
|
if not stage_class_path or not email_stage_pk:
|
||||||
stage: EmailStage | AuthenticatorEmailStage = stage_class(use_global_settings=True)
|
stage = EmailStage(use_global_settings=True)
|
||||||
else:
|
else:
|
||||||
|
stage_class = path_to_class(stage_class_path)
|
||||||
stages = stage_class.objects.filter(pk=email_stage_pk)
|
stages = stage_class.objects.filter(pk=email_stage_pk)
|
||||||
if not stages.exists():
|
if not stages.exists():
|
||||||
self.set_status(
|
self.set_status(
|
||||||
|
@ -8,7 +8,7 @@ from django.core.mail.backends.locmem import EmailBackend
|
|||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from authentik.core.models import User
|
from authentik.core.models import User
|
||||||
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
|
from authentik.core.tests.utils import create_test_admin_user, create_test_flow, create_test_user
|
||||||
from authentik.events.models import Event, EventAction
|
from authentik.events.models import Event, EventAction
|
||||||
from authentik.flows.markers import StageMarker
|
from authentik.flows.markers import StageMarker
|
||||||
from authentik.flows.models import FlowDesignation, FlowStageBinding
|
from authentik.flows.models import FlowDesignation, FlowStageBinding
|
||||||
@ -67,6 +67,36 @@ class TestEmailStageSending(FlowTestCase):
|
|||||||
self.assertEqual(event.context["to_email"], [f"{self.user.name} <{self.user.email}>"])
|
self.assertEqual(event.context["to_email"], [f"{self.user.name} <{self.user.email}>"])
|
||||||
self.assertEqual(event.context["from_email"], "system@authentik.local")
|
self.assertEqual(event.context["from_email"], "system@authentik.local")
|
||||||
|
|
||||||
|
def test_newlines_long_name(self):
|
||||||
|
"""Test with pending user"""
|
||||||
|
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
||||||
|
long_user = create_test_user()
|
||||||
|
long_user.name = "Test User\r\n Many Words\r\n"
|
||||||
|
long_user.save()
|
||||||
|
plan.context[PLAN_CONTEXT_PENDING_USER] = long_user
|
||||||
|
session = self.client.session
|
||||||
|
session[SESSION_KEY_PLAN] = plan
|
||||||
|
session.save()
|
||||||
|
Event.objects.filter(action=EventAction.EMAIL_SENT).delete()
|
||||||
|
|
||||||
|
url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
|
||||||
|
with patch(
|
||||||
|
"authentik.stages.email.models.EmailStage.backend_class",
|
||||||
|
PropertyMock(return_value=EmailBackend),
|
||||||
|
):
|
||||||
|
response = self.client.post(url)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertStageResponse(
|
||||||
|
response,
|
||||||
|
self.flow,
|
||||||
|
response_errors={
|
||||||
|
"non_field_errors": [{"string": "email-sent", "code": "email-sent"}]
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(len(mail.outbox), 1)
|
||||||
|
self.assertEqual(mail.outbox[0].subject, "authentik")
|
||||||
|
self.assertEqual(mail.outbox[0].to, [f"Test User Many Words <{long_user.email}>"])
|
||||||
|
|
||||||
def test_pending_fake_user(self):
|
def test_pending_fake_user(self):
|
||||||
"""Test with pending (fake) user"""
|
"""Test with pending (fake) user"""
|
||||||
self.flow.designation = FlowDesignation.RECOVERY
|
self.flow.designation = FlowDesignation.RECOVERY
|
||||||
|
58
authentik/stages/email/tests/test_tasks.py
Normal file
58
authentik/stages/email/tests/test_tasks.py
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
"""Test email stage tasks"""
|
||||||
|
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from django.core.mail import EmailMultiAlternatives
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from authentik.core.tests.utils import create_test_admin_user
|
||||||
|
from authentik.lib.utils.reflection import class_to_path
|
||||||
|
from authentik.stages.authenticator_email.models import AuthenticatorEmailStage
|
||||||
|
from authentik.stages.email.models import EmailStage
|
||||||
|
from authentik.stages.email.tasks import get_email_body, send_mails
|
||||||
|
|
||||||
|
|
||||||
|
class TestEmailTasks(TestCase):
|
||||||
|
"""Test email stage tasks"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.user = create_test_admin_user()
|
||||||
|
self.stage = EmailStage.objects.create(
|
||||||
|
name="test-email",
|
||||||
|
use_global_settings=True,
|
||||||
|
)
|
||||||
|
self.auth_stage = AuthenticatorEmailStage.objects.create(
|
||||||
|
name="test-auth-email",
|
||||||
|
use_global_settings=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_get_email_body_html(self):
|
||||||
|
"""Test get_email_body with HTML alternative"""
|
||||||
|
message = EmailMultiAlternatives()
|
||||||
|
message.body = "plain text"
|
||||||
|
message.attach_alternative("<p>html content</p>", "text/html")
|
||||||
|
self.assertEqual(get_email_body(message), "<p>html content</p>")
|
||||||
|
|
||||||
|
def test_get_email_body_plain(self):
|
||||||
|
"""Test get_email_body with plain text only"""
|
||||||
|
message = EmailMultiAlternatives()
|
||||||
|
message.body = "plain text"
|
||||||
|
self.assertEqual(get_email_body(message), "plain text")
|
||||||
|
|
||||||
|
def test_send_mails_email_stage(self):
|
||||||
|
"""Test send_mails with EmailStage"""
|
||||||
|
message = EmailMultiAlternatives()
|
||||||
|
with patch("authentik.stages.email.tasks.send_mail") as mock_send:
|
||||||
|
send_mails(self.stage, message)
|
||||||
|
mock_send.s.assert_called_once_with(
|
||||||
|
message.__dict__, class_to_path(EmailStage), str(self.stage.pk)
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_send_mails_authenticator_stage(self):
|
||||||
|
"""Test send_mails with AuthenticatorEmailStage"""
|
||||||
|
message = EmailMultiAlternatives()
|
||||||
|
with patch("authentik.stages.email.tasks.send_mail") as mock_send:
|
||||||
|
send_mails(self.auth_stage, message)
|
||||||
|
mock_send.s.assert_called_once_with(
|
||||||
|
message.__dict__, class_to_path(AuthenticatorEmailStage), str(self.auth_stage.pk)
|
||||||
|
)
|
@ -32,7 +32,14 @@ class TemplateEmailMessage(EmailMultiAlternatives):
|
|||||||
sanitized_to = []
|
sanitized_to = []
|
||||||
# Ensure that all recipients are valid
|
# Ensure that all recipients are valid
|
||||||
for recipient_name, recipient_email in to:
|
for recipient_name, recipient_email in to:
|
||||||
sanitized_to.append(sanitize_address((recipient_name, recipient_email), "utf-8"))
|
# Remove any newline characters from name and email before sanitizing
|
||||||
|
clean_name = (
|
||||||
|
recipient_name.replace("\n", " ").replace("\r", " ") if recipient_name else ""
|
||||||
|
)
|
||||||
|
clean_email = (
|
||||||
|
recipient_email.replace("\n", "").replace("\r", "") if recipient_email else ""
|
||||||
|
)
|
||||||
|
sanitized_to.append(sanitize_address((clean_name, clean_email), "utf-8"))
|
||||||
super().__init__(to=sanitized_to, **kwargs)
|
super().__init__(to=sanitized_to, **kwargs)
|
||||||
if not template_name:
|
if not template_name:
|
||||||
return
|
return
|
||||||
|
@ -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.12.3 Blueprint schema",
|
"title": "authentik 2025.2.3 Blueprint schema",
|
||||||
"required": [
|
"required": [
|
||||||
"version",
|
"version",
|
||||||
"entries"
|
"entries"
|
||||||
@ -801,6 +801,126 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"model",
|
||||||
|
"identifiers"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"model": {
|
||||||
|
"const": "authentik_providers_rac.racprovider"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"state": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"absent",
|
||||||
|
"present",
|
||||||
|
"created",
|
||||||
|
"must_created"
|
||||||
|
],
|
||||||
|
"default": "present"
|
||||||
|
},
|
||||||
|
"conditions": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"permissions": {
|
||||||
|
"$ref": "#/$defs/model_authentik_providers_rac.racprovider_permissions"
|
||||||
|
},
|
||||||
|
"attrs": {
|
||||||
|
"$ref": "#/$defs/model_authentik_providers_rac.racprovider"
|
||||||
|
},
|
||||||
|
"identifiers": {
|
||||||
|
"$ref": "#/$defs/model_authentik_providers_rac.racprovider"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"model",
|
||||||
|
"identifiers"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"model": {
|
||||||
|
"const": "authentik_providers_rac.endpoint"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"state": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"absent",
|
||||||
|
"present",
|
||||||
|
"created",
|
||||||
|
"must_created"
|
||||||
|
],
|
||||||
|
"default": "present"
|
||||||
|
},
|
||||||
|
"conditions": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"permissions": {
|
||||||
|
"$ref": "#/$defs/model_authentik_providers_rac.endpoint_permissions"
|
||||||
|
},
|
||||||
|
"attrs": {
|
||||||
|
"$ref": "#/$defs/model_authentik_providers_rac.endpoint"
|
||||||
|
},
|
||||||
|
"identifiers": {
|
||||||
|
"$ref": "#/$defs/model_authentik_providers_rac.endpoint"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"model",
|
||||||
|
"identifiers"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"model": {
|
||||||
|
"const": "authentik_providers_rac.racpropertymapping"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"state": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"absent",
|
||||||
|
"present",
|
||||||
|
"created",
|
||||||
|
"must_created"
|
||||||
|
],
|
||||||
|
"default": "present"
|
||||||
|
},
|
||||||
|
"conditions": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"permissions": {
|
||||||
|
"$ref": "#/$defs/model_authentik_providers_rac.racpropertymapping_permissions"
|
||||||
|
},
|
||||||
|
"attrs": {
|
||||||
|
"$ref": "#/$defs/model_authentik_providers_rac.racpropertymapping"
|
||||||
|
},
|
||||||
|
"identifiers": {
|
||||||
|
"$ref": "#/$defs/model_authentik_providers_rac.racpropertymapping"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
@ -3561,126 +3681,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"type": "object",
|
|
||||||
"required": [
|
|
||||||
"model",
|
|
||||||
"identifiers"
|
|
||||||
],
|
|
||||||
"properties": {
|
|
||||||
"model": {
|
|
||||||
"const": "authentik_providers_rac.racprovider"
|
|
||||||
},
|
|
||||||
"id": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"state": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": [
|
|
||||||
"absent",
|
|
||||||
"present",
|
|
||||||
"created",
|
|
||||||
"must_created"
|
|
||||||
],
|
|
||||||
"default": "present"
|
|
||||||
},
|
|
||||||
"conditions": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "boolean"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"permissions": {
|
|
||||||
"$ref": "#/$defs/model_authentik_providers_rac.racprovider_permissions"
|
|
||||||
},
|
|
||||||
"attrs": {
|
|
||||||
"$ref": "#/$defs/model_authentik_providers_rac.racprovider"
|
|
||||||
},
|
|
||||||
"identifiers": {
|
|
||||||
"$ref": "#/$defs/model_authentik_providers_rac.racprovider"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "object",
|
|
||||||
"required": [
|
|
||||||
"model",
|
|
||||||
"identifiers"
|
|
||||||
],
|
|
||||||
"properties": {
|
|
||||||
"model": {
|
|
||||||
"const": "authentik_providers_rac.endpoint"
|
|
||||||
},
|
|
||||||
"id": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"state": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": [
|
|
||||||
"absent",
|
|
||||||
"present",
|
|
||||||
"created",
|
|
||||||
"must_created"
|
|
||||||
],
|
|
||||||
"default": "present"
|
|
||||||
},
|
|
||||||
"conditions": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "boolean"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"permissions": {
|
|
||||||
"$ref": "#/$defs/model_authentik_providers_rac.endpoint_permissions"
|
|
||||||
},
|
|
||||||
"attrs": {
|
|
||||||
"$ref": "#/$defs/model_authentik_providers_rac.endpoint"
|
|
||||||
},
|
|
||||||
"identifiers": {
|
|
||||||
"$ref": "#/$defs/model_authentik_providers_rac.endpoint"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "object",
|
|
||||||
"required": [
|
|
||||||
"model",
|
|
||||||
"identifiers"
|
|
||||||
],
|
|
||||||
"properties": {
|
|
||||||
"model": {
|
|
||||||
"const": "authentik_providers_rac.racpropertymapping"
|
|
||||||
},
|
|
||||||
"id": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"state": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": [
|
|
||||||
"absent",
|
|
||||||
"present",
|
|
||||||
"created",
|
|
||||||
"must_created"
|
|
||||||
],
|
|
||||||
"default": "present"
|
|
||||||
},
|
|
||||||
"conditions": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "boolean"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"permissions": {
|
|
||||||
"$ref": "#/$defs/model_authentik_providers_rac.racpropertymapping_permissions"
|
|
||||||
},
|
|
||||||
"attrs": {
|
|
||||||
"$ref": "#/$defs/model_authentik_providers_rac.racpropertymapping"
|
|
||||||
},
|
|
||||||
"identifiers": {
|
|
||||||
"$ref": "#/$defs/model_authentik_providers_rac.racpropertymapping"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
@ -4663,6 +4663,7 @@
|
|||||||
"authentik.providers.ldap",
|
"authentik.providers.ldap",
|
||||||
"authentik.providers.oauth2",
|
"authentik.providers.oauth2",
|
||||||
"authentik.providers.proxy",
|
"authentik.providers.proxy",
|
||||||
|
"authentik.providers.rac",
|
||||||
"authentik.providers.radius",
|
"authentik.providers.radius",
|
||||||
"authentik.providers.saml",
|
"authentik.providers.saml",
|
||||||
"authentik.providers.scim",
|
"authentik.providers.scim",
|
||||||
@ -4703,7 +4704,6 @@
|
|||||||
"authentik.enterprise.audit",
|
"authentik.enterprise.audit",
|
||||||
"authentik.enterprise.providers.google_workspace",
|
"authentik.enterprise.providers.google_workspace",
|
||||||
"authentik.enterprise.providers.microsoft_entra",
|
"authentik.enterprise.providers.microsoft_entra",
|
||||||
"authentik.enterprise.providers.rac",
|
|
||||||
"authentik.enterprise.providers.ssf",
|
"authentik.enterprise.providers.ssf",
|
||||||
"authentik.enterprise.stages.authenticator_endpoint_gdtc",
|
"authentik.enterprise.stages.authenticator_endpoint_gdtc",
|
||||||
"authentik.enterprise.stages.source",
|
"authentik.enterprise.stages.source",
|
||||||
@ -4738,6 +4738,9 @@
|
|||||||
"authentik_providers_oauth2.scopemapping",
|
"authentik_providers_oauth2.scopemapping",
|
||||||
"authentik_providers_oauth2.oauth2provider",
|
"authentik_providers_oauth2.oauth2provider",
|
||||||
"authentik_providers_proxy.proxyprovider",
|
"authentik_providers_proxy.proxyprovider",
|
||||||
|
"authentik_providers_rac.racprovider",
|
||||||
|
"authentik_providers_rac.endpoint",
|
||||||
|
"authentik_providers_rac.racpropertymapping",
|
||||||
"authentik_providers_radius.radiusprovider",
|
"authentik_providers_radius.radiusprovider",
|
||||||
"authentik_providers_radius.radiusproviderpropertymapping",
|
"authentik_providers_radius.radiusproviderpropertymapping",
|
||||||
"authentik_providers_saml.samlprovider",
|
"authentik_providers_saml.samlprovider",
|
||||||
@ -4807,9 +4810,6 @@
|
|||||||
"authentik_providers_google_workspace.googleworkspaceprovidermapping",
|
"authentik_providers_google_workspace.googleworkspaceprovidermapping",
|
||||||
"authentik_providers_microsoft_entra.microsoftentraprovider",
|
"authentik_providers_microsoft_entra.microsoftentraprovider",
|
||||||
"authentik_providers_microsoft_entra.microsoftentraprovidermapping",
|
"authentik_providers_microsoft_entra.microsoftentraprovidermapping",
|
||||||
"authentik_providers_rac.racprovider",
|
|
||||||
"authentik_providers_rac.endpoint",
|
|
||||||
"authentik_providers_rac.racpropertymapping",
|
|
||||||
"authentik_providers_ssf.ssfprovider",
|
"authentik_providers_ssf.ssfprovider",
|
||||||
"authentik_stages_authenticator_endpoint_gdtc.authenticatorendpointgdtcstage",
|
"authentik_stages_authenticator_endpoint_gdtc.authenticatorendpointgdtcstage",
|
||||||
"authentik_stages_source.sourcestage",
|
"authentik_stages_source.sourcestage",
|
||||||
@ -6046,6 +6046,216 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"model_authentik_providers_rac.racprovider": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"title": "Name"
|
||||||
|
},
|
||||||
|
"authentication_flow": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid",
|
||||||
|
"title": "Authentication flow",
|
||||||
|
"description": "Flow used for authentication when the associated application is accessed by an un-authenticated user."
|
||||||
|
},
|
||||||
|
"authorization_flow": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid",
|
||||||
|
"title": "Authorization flow",
|
||||||
|
"description": "Flow used when authorizing this provider."
|
||||||
|
},
|
||||||
|
"property_mappings": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid"
|
||||||
|
},
|
||||||
|
"title": "Property mappings"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true,
|
||||||
|
"title": "Settings"
|
||||||
|
},
|
||||||
|
"connection_expiry": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"title": "Connection expiry",
|
||||||
|
"description": "Determines how long a session lasts. Default of 0 means that the sessions lasts until the browser is closed. (Format: hours=-1;minutes=-2;seconds=-3)"
|
||||||
|
},
|
||||||
|
"delete_token_on_disconnect": {
|
||||||
|
"type": "boolean",
|
||||||
|
"title": "Delete token on disconnect",
|
||||||
|
"description": "When set to true, connection tokens will be deleted upon disconnect."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": []
|
||||||
|
},
|
||||||
|
"model_authentik_providers_rac.racprovider_permissions": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"permission"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"permission": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"add_racprovider",
|
||||||
|
"change_racprovider",
|
||||||
|
"delete_racprovider",
|
||||||
|
"view_racprovider"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"user": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"model_authentik_providers_rac.endpoint": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"title": "Name"
|
||||||
|
},
|
||||||
|
"provider": {
|
||||||
|
"type": "integer",
|
||||||
|
"title": "Provider"
|
||||||
|
},
|
||||||
|
"protocol": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"rdp",
|
||||||
|
"vnc",
|
||||||
|
"ssh"
|
||||||
|
],
|
||||||
|
"title": "Protocol"
|
||||||
|
},
|
||||||
|
"host": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"title": "Host"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true,
|
||||||
|
"title": "Settings"
|
||||||
|
},
|
||||||
|
"property_mappings": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid"
|
||||||
|
},
|
||||||
|
"title": "Property mappings"
|
||||||
|
},
|
||||||
|
"auth_mode": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"static",
|
||||||
|
"prompt"
|
||||||
|
],
|
||||||
|
"title": "Auth mode"
|
||||||
|
},
|
||||||
|
"maximum_connections": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": -2147483648,
|
||||||
|
"maximum": 2147483647,
|
||||||
|
"title": "Maximum connections"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": []
|
||||||
|
},
|
||||||
|
"model_authentik_providers_rac.endpoint_permissions": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"permission"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"permission": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"add_endpoint",
|
||||||
|
"change_endpoint",
|
||||||
|
"delete_endpoint",
|
||||||
|
"view_endpoint"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"user": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"model_authentik_providers_rac.racpropertymapping": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"managed": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"minLength": 1,
|
||||||
|
"title": "Managed by authentik",
|
||||||
|
"description": "Objects that are managed by authentik. These objects are created and updated automatically. This flag only indicates that an object can be overwritten by migrations. You can still modify the objects via the API, but expect changes to be overwritten in a later update."
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"title": "Name"
|
||||||
|
},
|
||||||
|
"expression": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Expression"
|
||||||
|
},
|
||||||
|
"static_settings": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true,
|
||||||
|
"title": "Static settings"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": []
|
||||||
|
},
|
||||||
|
"model_authentik_providers_rac.racpropertymapping_permissions": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"permission"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"permission": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"add_racpropertymapping",
|
||||||
|
"change_racpropertymapping",
|
||||||
|
"delete_racpropertymapping",
|
||||||
|
"view_racpropertymapping"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"user": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"model_authentik_providers_radius.radiusprovider": {
|
"model_authentik_providers_radius.radiusprovider": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@ -14215,216 +14425,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"model_authentik_providers_rac.racprovider": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"name": {
|
|
||||||
"type": "string",
|
|
||||||
"minLength": 1,
|
|
||||||
"title": "Name"
|
|
||||||
},
|
|
||||||
"authentication_flow": {
|
|
||||||
"type": "string",
|
|
||||||
"format": "uuid",
|
|
||||||
"title": "Authentication flow",
|
|
||||||
"description": "Flow used for authentication when the associated application is accessed by an un-authenticated user."
|
|
||||||
},
|
|
||||||
"authorization_flow": {
|
|
||||||
"type": "string",
|
|
||||||
"format": "uuid",
|
|
||||||
"title": "Authorization flow",
|
|
||||||
"description": "Flow used when authorizing this provider."
|
|
||||||
},
|
|
||||||
"property_mappings": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "string",
|
|
||||||
"format": "uuid"
|
|
||||||
},
|
|
||||||
"title": "Property mappings"
|
|
||||||
},
|
|
||||||
"settings": {
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": true,
|
|
||||||
"title": "Settings"
|
|
||||||
},
|
|
||||||
"connection_expiry": {
|
|
||||||
"type": "string",
|
|
||||||
"minLength": 1,
|
|
||||||
"title": "Connection expiry",
|
|
||||||
"description": "Determines how long a session lasts. Default of 0 means that the sessions lasts until the browser is closed. (Format: hours=-1;minutes=-2;seconds=-3)"
|
|
||||||
},
|
|
||||||
"delete_token_on_disconnect": {
|
|
||||||
"type": "boolean",
|
|
||||||
"title": "Delete token on disconnect",
|
|
||||||
"description": "When set to true, connection tokens will be deleted upon disconnect."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": []
|
|
||||||
},
|
|
||||||
"model_authentik_providers_rac.racprovider_permissions": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "object",
|
|
||||||
"required": [
|
|
||||||
"permission"
|
|
||||||
],
|
|
||||||
"properties": {
|
|
||||||
"permission": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": [
|
|
||||||
"add_racprovider",
|
|
||||||
"change_racprovider",
|
|
||||||
"delete_racprovider",
|
|
||||||
"view_racprovider"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"user": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"role": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"model_authentik_providers_rac.endpoint": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"name": {
|
|
||||||
"type": "string",
|
|
||||||
"minLength": 1,
|
|
||||||
"title": "Name"
|
|
||||||
},
|
|
||||||
"provider": {
|
|
||||||
"type": "integer",
|
|
||||||
"title": "Provider"
|
|
||||||
},
|
|
||||||
"protocol": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": [
|
|
||||||
"rdp",
|
|
||||||
"vnc",
|
|
||||||
"ssh"
|
|
||||||
],
|
|
||||||
"title": "Protocol"
|
|
||||||
},
|
|
||||||
"host": {
|
|
||||||
"type": "string",
|
|
||||||
"minLength": 1,
|
|
||||||
"title": "Host"
|
|
||||||
},
|
|
||||||
"settings": {
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": true,
|
|
||||||
"title": "Settings"
|
|
||||||
},
|
|
||||||
"property_mappings": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "string",
|
|
||||||
"format": "uuid"
|
|
||||||
},
|
|
||||||
"title": "Property mappings"
|
|
||||||
},
|
|
||||||
"auth_mode": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": [
|
|
||||||
"static",
|
|
||||||
"prompt"
|
|
||||||
],
|
|
||||||
"title": "Auth mode"
|
|
||||||
},
|
|
||||||
"maximum_connections": {
|
|
||||||
"type": "integer",
|
|
||||||
"minimum": -2147483648,
|
|
||||||
"maximum": 2147483647,
|
|
||||||
"title": "Maximum connections"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": []
|
|
||||||
},
|
|
||||||
"model_authentik_providers_rac.endpoint_permissions": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "object",
|
|
||||||
"required": [
|
|
||||||
"permission"
|
|
||||||
],
|
|
||||||
"properties": {
|
|
||||||
"permission": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": [
|
|
||||||
"add_endpoint",
|
|
||||||
"change_endpoint",
|
|
||||||
"delete_endpoint",
|
|
||||||
"view_endpoint"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"user": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"role": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"model_authentik_providers_rac.racpropertymapping": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"managed": {
|
|
||||||
"type": [
|
|
||||||
"string",
|
|
||||||
"null"
|
|
||||||
],
|
|
||||||
"minLength": 1,
|
|
||||||
"title": "Managed by authentik",
|
|
||||||
"description": "Objects that are managed by authentik. These objects are created and updated automatically. This flag only indicates that an object can be overwritten by migrations. You can still modify the objects via the API, but expect changes to be overwritten in a later update."
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"type": "string",
|
|
||||||
"minLength": 1,
|
|
||||||
"title": "Name"
|
|
||||||
},
|
|
||||||
"expression": {
|
|
||||||
"type": "string",
|
|
||||||
"title": "Expression"
|
|
||||||
},
|
|
||||||
"static_settings": {
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": true,
|
|
||||||
"title": "Static settings"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": []
|
|
||||||
},
|
|
||||||
"model_authentik_providers_rac.racpropertymapping_permissions": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "object",
|
|
||||||
"required": [
|
|
||||||
"permission"
|
|
||||||
],
|
|
||||||
"properties": {
|
|
||||||
"permission": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": [
|
|
||||||
"add_racpropertymapping",
|
|
||||||
"change_racpropertymapping",
|
|
||||||
"delete_racpropertymapping",
|
|
||||||
"view_racpropertymapping"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"user": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"role": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"model_authentik_providers_ssf.ssfprovider": {
|
"model_authentik_providers_ssf.ssfprovider": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
@ -10,6 +10,7 @@ import (
|
|||||||
|
|
||||||
"goauthentik.io/internal/common"
|
"goauthentik.io/internal/common"
|
||||||
"goauthentik.io/internal/config"
|
"goauthentik.io/internal/config"
|
||||||
|
"goauthentik.io/internal/constants"
|
||||||
"goauthentik.io/internal/debug"
|
"goauthentik.io/internal/debug"
|
||||||
"goauthentik.io/internal/outpost/ak"
|
"goauthentik.io/internal/outpost/ak"
|
||||||
"goauthentik.io/internal/outpost/ak/healthcheck"
|
"goauthentik.io/internal/outpost/ak/healthcheck"
|
||||||
@ -24,7 +25,8 @@ Required environment variables:
|
|||||||
- AUTHENTIK_INSECURE: Skip SSL Certificate verification`
|
- AUTHENTIK_INSECURE: Skip SSL Certificate verification`
|
||||||
|
|
||||||
var rootCmd = &cobra.Command{
|
var rootCmd = &cobra.Command{
|
||||||
Long: helpMessage,
|
Long: helpMessage,
|
||||||
|
Version: constants.FullVersion(),
|
||||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
||||||
log.SetLevel(log.DebugLevel)
|
log.SetLevel(log.DebugLevel)
|
||||||
log.SetFormatter(&log.JSONFormatter{
|
log.SetFormatter(&log.JSONFormatter{
|
||||||
|
@ -10,6 +10,7 @@ import (
|
|||||||
|
|
||||||
"goauthentik.io/internal/common"
|
"goauthentik.io/internal/common"
|
||||||
"goauthentik.io/internal/config"
|
"goauthentik.io/internal/config"
|
||||||
|
"goauthentik.io/internal/constants"
|
||||||
"goauthentik.io/internal/debug"
|
"goauthentik.io/internal/debug"
|
||||||
"goauthentik.io/internal/outpost/ak"
|
"goauthentik.io/internal/outpost/ak"
|
||||||
"goauthentik.io/internal/outpost/ak/healthcheck"
|
"goauthentik.io/internal/outpost/ak/healthcheck"
|
||||||
@ -27,7 +28,8 @@ Optionally, you can set these:
|
|||||||
- AUTHENTIK_HOST_BROWSER: URL to use in the browser, when it differs from AUTHENTIK_HOST`
|
- AUTHENTIK_HOST_BROWSER: URL to use in the browser, when it differs from AUTHENTIK_HOST`
|
||||||
|
|
||||||
var rootCmd = &cobra.Command{
|
var rootCmd = &cobra.Command{
|
||||||
Long: helpMessage,
|
Long: helpMessage,
|
||||||
|
Version: constants.FullVersion(),
|
||||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
||||||
log.SetLevel(log.DebugLevel)
|
log.SetLevel(log.DebugLevel)
|
||||||
log.SetFormatter(&log.JSONFormatter{
|
log.SetFormatter(&log.JSONFormatter{
|
||||||
|
@ -9,6 +9,7 @@ import (
|
|||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
"goauthentik.io/internal/common"
|
"goauthentik.io/internal/common"
|
||||||
|
"goauthentik.io/internal/constants"
|
||||||
"goauthentik.io/internal/debug"
|
"goauthentik.io/internal/debug"
|
||||||
"goauthentik.io/internal/outpost/ak"
|
"goauthentik.io/internal/outpost/ak"
|
||||||
"goauthentik.io/internal/outpost/ak/healthcheck"
|
"goauthentik.io/internal/outpost/ak/healthcheck"
|
||||||
@ -23,7 +24,8 @@ Required environment variables:
|
|||||||
- AUTHENTIK_INSECURE: Skip SSL Certificate verification`
|
- AUTHENTIK_INSECURE: Skip SSL Certificate verification`
|
||||||
|
|
||||||
var rootCmd = &cobra.Command{
|
var rootCmd = &cobra.Command{
|
||||||
Long: helpMessage,
|
Long: helpMessage,
|
||||||
|
Version: constants.FullVersion(),
|
||||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
||||||
log.SetLevel(log.DebugLevel)
|
log.SetLevel(log.DebugLevel)
|
||||||
log.SetFormatter(&log.JSONFormatter{
|
log.SetFormatter(&log.JSONFormatter{
|
||||||
|
@ -9,6 +9,7 @@ import (
|
|||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
"goauthentik.io/internal/common"
|
"goauthentik.io/internal/common"
|
||||||
|
"goauthentik.io/internal/constants"
|
||||||
"goauthentik.io/internal/debug"
|
"goauthentik.io/internal/debug"
|
||||||
"goauthentik.io/internal/outpost/ak"
|
"goauthentik.io/internal/outpost/ak"
|
||||||
"goauthentik.io/internal/outpost/ak/healthcheck"
|
"goauthentik.io/internal/outpost/ak/healthcheck"
|
||||||
@ -23,7 +24,8 @@ Required environment variables:
|
|||||||
- AUTHENTIK_INSECURE: Skip SSL Certificate verification`
|
- AUTHENTIK_INSECURE: Skip SSL Certificate verification`
|
||||||
|
|
||||||
var rootCmd = &cobra.Command{
|
var rootCmd = &cobra.Command{
|
||||||
Long: helpMessage,
|
Long: helpMessage,
|
||||||
|
Version: constants.FullVersion(),
|
||||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
||||||
log.SetLevel(log.DebugLevel)
|
log.SetLevel(log.DebugLevel)
|
||||||
log.SetFormatter(&log.JSONFormatter{
|
log.SetFormatter(&log.JSONFormatter{
|
||||||
|
@ -31,7 +31,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- redis:/data
|
- redis:/data
|
||||||
server:
|
server:
|
||||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.12.3}
|
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.2.3}
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command: server
|
command: server
|
||||||
environment:
|
environment:
|
||||||
@ -54,7 +54,7 @@ services:
|
|||||||
redis:
|
redis:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
worker:
|
worker:
|
||||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.12.3}
|
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.2.3}
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command: worker
|
command: worker
|
||||||
environment:
|
environment:
|
||||||
|
@ -29,4 +29,4 @@ func UserAgent() string {
|
|||||||
return fmt.Sprintf("authentik@%s", FullVersion())
|
return fmt.Sprintf("authentik@%s", FullVersion())
|
||||||
}
|
}
|
||||||
|
|
||||||
const VERSION = "2024.12.3"
|
const VERSION = "2025.2.3"
|
||||||
|
@ -35,13 +35,19 @@ func Paginator[Tobj any, Treq any, Tres PaginatorResponse[Tobj]](
|
|||||||
req PaginatorRequest[Treq, Tres],
|
req PaginatorRequest[Treq, Tres],
|
||||||
opts PaginatorOptions,
|
opts PaginatorOptions,
|
||||||
) ([]Tobj, error) {
|
) ([]Tobj, error) {
|
||||||
|
if opts.Logger == nil {
|
||||||
|
opts.Logger = log.NewEntry(log.StandardLogger())
|
||||||
|
}
|
||||||
var bfreq, cfreq interface{}
|
var bfreq, cfreq interface{}
|
||||||
fetchOffset := func(page int32) (Tres, error) {
|
fetchOffset := func(page int32) (Tres, error) {
|
||||||
bfreq = req.Page(page)
|
bfreq = req.Page(page)
|
||||||
cfreq = bfreq.(PaginatorRequest[Treq, Tres]).PageSize(int32(opts.PageSize))
|
cfreq = bfreq.(PaginatorRequest[Treq, Tres]).PageSize(int32(opts.PageSize))
|
||||||
res, _, err := cfreq.(PaginatorRequest[Treq, Tres]).Execute()
|
res, hres, err := cfreq.(PaginatorRequest[Treq, Tres]).Execute()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
opts.Logger.WithError(err).WithField("page", page).Warning("failed to fetch page")
|
opts.Logger.WithError(err).WithField("page", page).Warning("failed to fetch page")
|
||||||
|
if hres != nil && hres.StatusCode >= 400 && hres.StatusCode < 500 {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return res, err
|
return res, err
|
||||||
}
|
}
|
||||||
@ -51,6 +57,9 @@ func Paginator[Tobj any, Treq any, Tres PaginatorResponse[Tobj]](
|
|||||||
for {
|
for {
|
||||||
apiObjects, err := fetchOffset(page)
|
apiObjects, err := fetchOffset(page)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if page == 1 {
|
||||||
|
return objects, err
|
||||||
|
}
|
||||||
errs = append(errs, err)
|
errs = append(errs, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,64 @@
|
|||||||
package ak
|
package ak
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"goauthentik.io/api/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type fakeAPIType struct{}
|
||||||
|
|
||||||
|
type fakeAPIResponse struct {
|
||||||
|
results []fakeAPIType
|
||||||
|
pagination api.Pagination
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fapi *fakeAPIResponse) GetResults() []fakeAPIType { return fapi.results }
|
||||||
|
func (fapi *fakeAPIResponse) GetPagination() api.Pagination { return fapi.pagination }
|
||||||
|
|
||||||
|
type fakeAPIRequest struct {
|
||||||
|
res *fakeAPIResponse
|
||||||
|
http *http.Response
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fapi *fakeAPIRequest) Page(page int32) *fakeAPIRequest { return fapi }
|
||||||
|
func (fapi *fakeAPIRequest) PageSize(size int32) *fakeAPIRequest { return fapi }
|
||||||
|
func (fapi *fakeAPIRequest) Execute() (*fakeAPIResponse, *http.Response, error) {
|
||||||
|
return fapi.res, fapi.http, fapi.err
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_Simple(t *testing.T) {
|
||||||
|
req := &fakeAPIRequest{
|
||||||
|
res: &fakeAPIResponse{
|
||||||
|
results: []fakeAPIType{
|
||||||
|
{},
|
||||||
|
},
|
||||||
|
pagination: api.Pagination{
|
||||||
|
TotalPages: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
res, err := Paginator(req, PaginatorOptions{})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, res, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_BadRequest(t *testing.T) {
|
||||||
|
req := &fakeAPIRequest{
|
||||||
|
http: &http.Response{
|
||||||
|
StatusCode: 400,
|
||||||
|
},
|
||||||
|
err: errors.New("foo"),
|
||||||
|
}
|
||||||
|
res, err := Paginator(req, PaginatorOptions{})
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Equal(t, []fakeAPIType{}, res)
|
||||||
|
}
|
||||||
|
|
||||||
// func Test_PaginatorCompile(t *testing.T) {
|
// func Test_PaginatorCompile(t *testing.T) {
|
||||||
// req := api.ApiCoreUsersListRequest{}
|
// req := api.ApiCoreUsersListRequest{}
|
||||||
// Paginator(req, PaginatorOptions{
|
// Paginator(req, PaginatorOptions{
|
||||||
|
@ -26,7 +26,7 @@ Parameters:
|
|||||||
Description: authentik Docker image
|
Description: authentik Docker image
|
||||||
AuthentikVersion:
|
AuthentikVersion:
|
||||||
Type: String
|
Type: String
|
||||||
Default: 2024.12.3
|
Default: 2025.2.3
|
||||||
Description: authentik Docker image tag
|
Description: authentik Docker image tag
|
||||||
AuthentikServerCPU:
|
AuthentikServerCPU:
|
||||||
Type: Number
|
Type: Number
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "@goauthentik/authentik",
|
"name": "@goauthentik/authentik",
|
||||||
"version": "2024.12.3",
|
"version": "2025.2.3",
|
||||||
"private": true
|
"private": true
|
||||||
}
|
}
|
||||||
|
368
poetry.lock
generated
368
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.12.3"
|
version = "2025.2.3"
|
||||||
description = ""
|
description = ""
|
||||||
authors = ["authentik Team <hello@goauthentik.io>"]
|
authors = ["authentik Team <hello@goauthentik.io>"]
|
||||||
|
|
||||||
@ -123,7 +123,7 @@ kubernetes = "*"
|
|||||||
ldap3 = "*"
|
ldap3 = "*"
|
||||||
lxml = "*"
|
lxml = "*"
|
||||||
msgraph-sdk = "*"
|
msgraph-sdk = "*"
|
||||||
opencontainers = { git = "https://github.com/vsoch/oci-python", rev = "20d69d9cc50a0fef31605b46f06da0c94f1ec3cf", extras = ["reggie"] }
|
opencontainers = { git = "https://github.com/BeryJu/oci-python", rev = "c791b19056769cd67957322806809ab70f5bead8", extras = ["reggie"] }
|
||||||
packaging = "*"
|
packaging = "*"
|
||||||
paramiko = "*"
|
paramiko = "*"
|
||||||
psycopg = { extras = ["c"], version = "*" }
|
psycopg = { extras = ["c"], version = "*" }
|
||||||
|
10
schema.yml
10
schema.yml
@ -1,7 +1,7 @@
|
|||||||
openapi: 3.0.3
|
openapi: 3.0.3
|
||||||
info:
|
info:
|
||||||
title: authentik
|
title: authentik
|
||||||
version: 2024.12.3
|
version: 2025.2.3
|
||||||
description: Making authentication simple.
|
description: Making authentication simple.
|
||||||
contact:
|
contact:
|
||||||
email: hello@goauthentik.io
|
email: hello@goauthentik.io
|
||||||
@ -39482,6 +39482,7 @@ components:
|
|||||||
- authentik.providers.ldap
|
- authentik.providers.ldap
|
||||||
- authentik.providers.oauth2
|
- authentik.providers.oauth2
|
||||||
- authentik.providers.proxy
|
- authentik.providers.proxy
|
||||||
|
- authentik.providers.rac
|
||||||
- authentik.providers.radius
|
- authentik.providers.radius
|
||||||
- authentik.providers.saml
|
- authentik.providers.saml
|
||||||
- authentik.providers.scim
|
- authentik.providers.scim
|
||||||
@ -39522,7 +39523,6 @@ components:
|
|||||||
- authentik.enterprise.audit
|
- authentik.enterprise.audit
|
||||||
- authentik.enterprise.providers.google_workspace
|
- authentik.enterprise.providers.google_workspace
|
||||||
- authentik.enterprise.providers.microsoft_entra
|
- authentik.enterprise.providers.microsoft_entra
|
||||||
- authentik.enterprise.providers.rac
|
|
||||||
- authentik.enterprise.providers.ssf
|
- authentik.enterprise.providers.ssf
|
||||||
- authentik.enterprise.stages.authenticator_endpoint_gdtc
|
- authentik.enterprise.stages.authenticator_endpoint_gdtc
|
||||||
- authentik.enterprise.stages.source
|
- authentik.enterprise.stages.source
|
||||||
@ -46625,6 +46625,9 @@ components:
|
|||||||
- authentik_providers_oauth2.scopemapping
|
- authentik_providers_oauth2.scopemapping
|
||||||
- authentik_providers_oauth2.oauth2provider
|
- authentik_providers_oauth2.oauth2provider
|
||||||
- authentik_providers_proxy.proxyprovider
|
- authentik_providers_proxy.proxyprovider
|
||||||
|
- authentik_providers_rac.racprovider
|
||||||
|
- authentik_providers_rac.endpoint
|
||||||
|
- authentik_providers_rac.racpropertymapping
|
||||||
- authentik_providers_radius.radiusprovider
|
- authentik_providers_radius.radiusprovider
|
||||||
- authentik_providers_radius.radiusproviderpropertymapping
|
- authentik_providers_radius.radiusproviderpropertymapping
|
||||||
- authentik_providers_saml.samlprovider
|
- authentik_providers_saml.samlprovider
|
||||||
@ -46694,9 +46697,6 @@ components:
|
|||||||
- authentik_providers_google_workspace.googleworkspaceprovidermapping
|
- authentik_providers_google_workspace.googleworkspaceprovidermapping
|
||||||
- authentik_providers_microsoft_entra.microsoftentraprovider
|
- authentik_providers_microsoft_entra.microsoftentraprovider
|
||||||
- authentik_providers_microsoft_entra.microsoftentraprovidermapping
|
- authentik_providers_microsoft_entra.microsoftentraprovidermapping
|
||||||
- authentik_providers_rac.racprovider
|
|
||||||
- authentik_providers_rac.endpoint
|
|
||||||
- authentik_providers_rac.racpropertymapping
|
|
||||||
- authentik_providers_ssf.ssfprovider
|
- authentik_providers_ssf.ssfprovider
|
||||||
- authentik_stages_authenticator_endpoint_gdtc.authenticatorendpointgdtcstage
|
- authentik_stages_authenticator_endpoint_gdtc.authenticatorendpointgdtcstage
|
||||||
- authentik_stages_source.sourcestage
|
- authentik_stages_source.sourcestage
|
||||||
|
@ -74,7 +74,7 @@ const interfaces = [
|
|||||||
["user/UserInterface.ts", "user"],
|
["user/UserInterface.ts", "user"],
|
||||||
["flow/FlowInterface.ts", "flow"],
|
["flow/FlowInterface.ts", "flow"],
|
||||||
["standalone/api-browser/index.ts", "standalone/api-browser"],
|
["standalone/api-browser/index.ts", "standalone/api-browser"],
|
||||||
["enterprise/rac/index.ts", "enterprise/rac"],
|
["rac/index.ts", "rac"],
|
||||||
["standalone/loading/index.ts", "standalone/loading"],
|
["standalone/loading/index.ts", "standalone/loading"],
|
||||||
["polyfill/poly.ts", "."],
|
["polyfill/poly.ts", "."],
|
||||||
];
|
];
|
||||||
|
8
web/package-lock.json
generated
8
web/package-lock.json
generated
@ -23,7 +23,7 @@
|
|||||||
"@floating-ui/dom": "^1.6.11",
|
"@floating-ui/dom": "^1.6.11",
|
||||||
"@formatjs/intl-listformat": "^7.5.7",
|
"@formatjs/intl-listformat": "^7.5.7",
|
||||||
"@fortawesome/fontawesome-free": "^6.6.0",
|
"@fortawesome/fontawesome-free": "^6.6.0",
|
||||||
"@goauthentik/api": "^2024.12.3-1739814462",
|
"@goauthentik/api": "^2024.12.3-1739965710",
|
||||||
"@lit-labs/ssr": "^3.2.2",
|
"@lit-labs/ssr": "^3.2.2",
|
||||||
"@lit/context": "^1.1.2",
|
"@lit/context": "^1.1.2",
|
||||||
"@lit/localize": "^0.12.2",
|
"@lit/localize": "^0.12.2",
|
||||||
@ -1814,9 +1814,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@goauthentik/api": {
|
"node_modules/@goauthentik/api": {
|
||||||
"version": "2024.12.3-1739814462",
|
"version": "2024.12.3-1739965710",
|
||||||
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2024.12.3-1739814462.tgz",
|
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2024.12.3-1739965710.tgz",
|
||||||
"integrity": "sha512-qWGsq7zP0rG1PfjZA+iimaX4cVkd1n2JA/WceTOKgBmqnomQSI7SJNkdSpD+Qdy76PI0UuQWN73PInq/3rmm5Q=="
|
"integrity": "sha512-16zoQWeJhAFSwttvqLRoXoQA43tMW1ZXDEihW6r8rtWtlxqPh7n36RtcWYraYiLcjmJskI90zdgz6k1kmY5AXw=="
|
||||||
},
|
},
|
||||||
"node_modules/@goauthentik/web": {
|
"node_modules/@goauthentik/web": {
|
||||||
"resolved": "",
|
"resolved": "",
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
"@floating-ui/dom": "^1.6.11",
|
"@floating-ui/dom": "^1.6.11",
|
||||||
"@formatjs/intl-listformat": "^7.5.7",
|
"@formatjs/intl-listformat": "^7.5.7",
|
||||||
"@fortawesome/fontawesome-free": "^6.6.0",
|
"@fortawesome/fontawesome-free": "^6.6.0",
|
||||||
"@goauthentik/api": "^2024.12.3-1739814462",
|
"@goauthentik/api": "^2024.12.3-1739965710",
|
||||||
"@lit-labs/ssr": "^3.2.2",
|
"@lit-labs/ssr": "^3.2.2",
|
||||||
"@lit/context": "^1.1.2",
|
"@lit/context": "^1.1.2",
|
||||||
"@lit/localize": "^0.12.2",
|
"@lit/localize": "^0.12.2",
|
||||||
|
@ -6,7 +6,7 @@ const config: KnipConfig = {
|
|||||||
"./src/user/UserInterface.ts",
|
"./src/user/UserInterface.ts",
|
||||||
"./src/flow/FlowInterface.ts",
|
"./src/flow/FlowInterface.ts",
|
||||||
"./src/standalone/api-browser/index.ts",
|
"./src/standalone/api-browser/index.ts",
|
||||||
"./src/enterprise/rac/index.ts",
|
"./src/rac/index.ts",
|
||||||
"./src/standalone/loading/index.ts",
|
"./src/standalone/loading/index.ts",
|
||||||
"./src/polyfill/poly.ts",
|
"./src/polyfill/poly.ts",
|
||||||
],
|
],
|
||||||
|
@ -7,6 +7,7 @@ import "@goauthentik/components/ak-radio-input";
|
|||||||
import "@goauthentik/components/ak-switch-input";
|
import "@goauthentik/components/ak-switch-input";
|
||||||
import "@goauthentik/components/ak-text-input";
|
import "@goauthentik/components/ak-text-input";
|
||||||
import "@goauthentik/components/ak-textarea-input";
|
import "@goauthentik/components/ak-textarea-input";
|
||||||
|
import "@goauthentik/elements/Alert.js";
|
||||||
import {
|
import {
|
||||||
CapabilitiesEnum,
|
CapabilitiesEnum,
|
||||||
WithCapabilitiesConfig,
|
WithCapabilitiesConfig,
|
||||||
@ -21,7 +22,7 @@ import "@goauthentik/elements/forms/SearchSelect";
|
|||||||
import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
|
import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
|
||||||
|
|
||||||
import { msg } from "@lit/localize";
|
import { msg } from "@lit/localize";
|
||||||
import { TemplateResult, html } from "lit";
|
import { TemplateResult, html, nothing } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators.js";
|
import { customElement, property, state } from "lit/decorators.js";
|
||||||
import { ifDefined } from "lit/directives/if-defined.js";
|
import { ifDefined } from "lit/directives/if-defined.js";
|
||||||
|
|
||||||
@ -120,7 +121,12 @@ export class ApplicationForm extends WithCapabilitiesConfig(ModelForm<Applicatio
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderForm(): TemplateResult {
|
renderForm(): TemplateResult {
|
||||||
|
const alertMsg = msg(
|
||||||
|
"Using this form will only create an Application. In order to authenticate with the application, you will have to manually pair it with a Provider.",
|
||||||
|
);
|
||||||
|
|
||||||
return html`<form class="pf-c-form pf-m-horizontal">
|
return html`<form class="pf-c-form pf-m-horizontal">
|
||||||
|
${this.instance ? nothing : html`<ak-alert level="pf-m-info">${alertMsg}</ak-alert>`}
|
||||||
<ak-text-input
|
<ak-text-input
|
||||||
name="name"
|
name="name"
|
||||||
value=${ifDefined(this.instance?.name)}
|
value=${ifDefined(this.instance?.name)}
|
||||||
|
@ -50,7 +50,7 @@ export class ApplicationListPage extends WithBrandConfig(TablePage<Application>)
|
|||||||
}
|
}
|
||||||
pageDescription(): string {
|
pageDescription(): string {
|
||||||
return msg(
|
return msg(
|
||||||
str`External applications that use ${this.brand.brandingTitle || "authentik"} as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.`,
|
str`External applications that use ${this.brand?.brandingTitle ?? "authentik"} as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
pageIcon(): string {
|
pageIcon(): string {
|
||||||
@ -85,10 +85,6 @@ export class ApplicationListPage extends WithBrandConfig(TablePage<Application>)
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
renderSectionBefore(): TemplateResult {
|
|
||||||
return html`<ak-application-wizard-hint></ak-application-wizard-hint>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
renderSidebarAfter(): TemplateResult {
|
renderSidebarAfter(): TemplateResult {
|
||||||
return html`<div class="pf-c-sidebar__panel pf-m-width-25">
|
return html`<div class="pf-c-sidebar__panel pf-m-width-25">
|
||||||
<div class="pf-c-card">
|
<div class="pf-c-card">
|
||||||
@ -160,12 +156,21 @@ export class ApplicationListPage extends WithBrandConfig(TablePage<Application>)
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderObjectCreate(): TemplateResult {
|
renderObjectCreate(): TemplateResult {
|
||||||
return html`<ak-forms-modal .open=${getURLParam("createForm", false)}>
|
return html` <ak-application-wizard .open=${getURLParam("createWizard", false)}>
|
||||||
<span slot="submit"> ${msg("Create")} </span>
|
<button
|
||||||
<span slot="header"> ${msg("Create Application")} </span>
|
slot="trigger"
|
||||||
<ak-application-form slot="form"> </ak-application-form>
|
class="pf-c-button pf-m-primary"
|
||||||
<button slot="trigger" class="pf-c-button pf-m-primary">${msg("Create")}</button>
|
data-ouia-component-id="start-application-wizard"
|
||||||
</ak-forms-modal>`;
|
>
|
||||||
|
${msg("Create with Provider")}
|
||||||
|
</button>
|
||||||
|
</ak-application-wizard>
|
||||||
|
<ak-forms-modal .open=${getURLParam("createForm", false)}>
|
||||||
|
<span slot="submit"> ${msg("Create")} </span>
|
||||||
|
<span slot="header"> ${msg("Create Application")} </span>
|
||||||
|
<ak-application-form slot="form"> </ak-application-form>
|
||||||
|
<button slot="trigger" class="pf-c-button pf-m-primary">${msg("Create")}</button>
|
||||||
|
</ak-forms-modal>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -94,7 +94,7 @@ export class ApplicationEntitlementsPage extends Table<ApplicationEntitlement> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderExpanded(item: ApplicationEntitlement): TemplateResult {
|
renderExpanded(item: ApplicationEntitlement): TemplateResult {
|
||||||
return html` <td></td>
|
return html`<td></td>
|
||||||
<td role="cell" colspan="4">
|
<td role="cell" colspan="4">
|
||||||
<div class="pf-c-table__expandable-row-content">
|
<div class="pf-c-table__expandable-row-content">
|
||||||
<div class="pf-c-content">
|
<div class="pf-c-content">
|
||||||
|
@ -30,7 +30,7 @@ export class ApplicationWizardStep extends WizardStep {
|
|||||||
// As recommended in [WizardStep](../../../components/ak-wizard/WizardStep.ts), we override
|
// As recommended in [WizardStep](../../../components/ak-wizard/WizardStep.ts), we override
|
||||||
// these fields and provide them to all the child classes.
|
// these fields and provide them to all the child classes.
|
||||||
wizardTitle = msg("New application");
|
wizardTitle = msg("New application");
|
||||||
wizardDescription = msg("Create a new application");
|
wizardDescription = msg("Create a new application and configure a provider for it.");
|
||||||
canCancel = true;
|
canCancel = true;
|
||||||
|
|
||||||
// This should be overridden in the children for more precise targeting.
|
// This should be overridden in the children for more precise targeting.
|
||||||
|
@ -58,7 +58,7 @@ export class ApplicationWizardBindingsStep extends ApplicationWizardStep {
|
|||||||
get bindingsAsColumns() {
|
get bindingsAsColumns() {
|
||||||
return this.wizard.bindings.map((binding, index) => {
|
return this.wizard.bindings.map((binding, index) => {
|
||||||
const { order, enabled, timeout } = binding;
|
const { order, enabled, timeout } = binding;
|
||||||
const isSet = P.string.minLength(1);
|
const isSet = P.union(P.string.minLength(1), P.number);
|
||||||
const policy = match(binding)
|
const policy = match(binding)
|
||||||
.with({ policy: isSet }, (v) => msg(str`Policy ${v.policyObj?.name}`))
|
.with({ policy: isSet }, (v) => msg(str`Policy ${v.policyObj?.name}`))
|
||||||
.with({ group: isSet }, (v) => msg(str`Group ${v.groupObj?.name}`))
|
.with({ group: isSet }, (v) => msg(str`Group ${v.groupObj?.name}`))
|
||||||
|
@ -31,9 +31,9 @@ export class BoundPoliciesList extends Table<PolicyBinding> {
|
|||||||
|
|
||||||
@property({ type: Array })
|
@property({ type: Array })
|
||||||
allowedTypes: PolicyBindingCheckTarget[] = [
|
allowedTypes: PolicyBindingCheckTarget[] = [
|
||||||
|
PolicyBindingCheckTarget.policy,
|
||||||
PolicyBindingCheckTarget.group,
|
PolicyBindingCheckTarget.group,
|
||||||
PolicyBindingCheckTarget.user,
|
PolicyBindingCheckTarget.user,
|
||||||
PolicyBindingCheckTarget.policy,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
@property({ type: Array })
|
@property({ type: Array })
|
||||||
|
@ -58,9 +58,9 @@ export class PolicyBindingForm extends ModelForm<PolicyBinding, string> {
|
|||||||
|
|
||||||
@property({ type: Array })
|
@property({ type: Array })
|
||||||
allowedTypes: PolicyBindingCheckTarget[] = [
|
allowedTypes: PolicyBindingCheckTarget[] = [
|
||||||
|
PolicyBindingCheckTarget.policy,
|
||||||
PolicyBindingCheckTarget.group,
|
PolicyBindingCheckTarget.group,
|
||||||
PolicyBindingCheckTarget.user,
|
PolicyBindingCheckTarget.user,
|
||||||
PolicyBindingCheckTarget.policy,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
@property({ type: Array })
|
@property({ type: Array })
|
||||||
|
@ -105,6 +105,22 @@ export class GeoIPPolicyForm extends BasePolicyForm<GeoIPPolicy> {
|
|||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
</ak-form-element-horizontal>
|
</ak-form-element-horizontal>
|
||||||
|
<ak-form-element-horizontal
|
||||||
|
label=${msg("Maximum distance")}
|
||||||
|
name="historyMaxDistanceKm"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
value="${first(this.instance?.historyMaxDistanceKm, 100)}"
|
||||||
|
class="pf-c-form-control"
|
||||||
|
/>
|
||||||
|
<p class="pf-c-form__helper-text">
|
||||||
|
${msg(
|
||||||
|
"Maximum distance a login attempt is allowed from in kilometers.",
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</ak-form-element-horizontal>
|
||||||
<ak-form-element-horizontal
|
<ak-form-element-horizontal
|
||||||
label=${msg("Distance tolerance")}
|
label=${msg("Distance tolerance")}
|
||||||
name="distanceToleranceKm"
|
name="distanceToleranceKm"
|
||||||
@ -133,27 +149,6 @@ export class GeoIPPolicyForm extends BasePolicyForm<GeoIPPolicy> {
|
|||||||
${msg("Amount of previous login events to check against.")}
|
${msg("Amount of previous login events to check against.")}
|
||||||
</p>
|
</p>
|
||||||
</ak-form-element-horizontal>
|
</ak-form-element-horizontal>
|
||||||
<ak-form-element-horizontal
|
|
||||||
label=${msg("Maximum distance")}
|
|
||||||
name="historyMaxDistanceKm"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
min="1"
|
|
||||||
value="${first(this.instance?.historyMaxDistanceKm, 100)}"
|
|
||||||
class="pf-c-form-control"
|
|
||||||
/>
|
|
||||||
<p class="pf-c-form__helper-text">
|
|
||||||
${msg(
|
|
||||||
"Maximum distance a login attempt is allowed from in kilometers.",
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
</ak-form-element-horizontal>
|
|
||||||
</div>
|
|
||||||
</ak-form-group>
|
|
||||||
<ak-form-group>
|
|
||||||
<span slot="header"> ${msg("Distance settings (Impossible travel)")} </span>
|
|
||||||
<div slot="body" class="pf-c-form">
|
|
||||||
<ak-form-element-horizontal name="checkImpossibleTravel">
|
<ak-form-element-horizontal name="checkImpossibleTravel">
|
||||||
<label class="pf-c-switch">
|
<label class="pf-c-switch">
|
||||||
<input
|
<input
|
||||||
|
@ -21,12 +21,22 @@ export class RelatedApplicationButton extends AKElement {
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
provider?: Provider;
|
provider?: Provider;
|
||||||
|
|
||||||
|
@property()
|
||||||
|
mode: "primary" | "backchannel" = "primary";
|
||||||
|
|
||||||
render(): TemplateResult {
|
render(): TemplateResult {
|
||||||
if (this.provider?.assignedApplicationSlug) {
|
if (this.mode === "primary" && this.provider?.assignedApplicationSlug) {
|
||||||
return html`<a href="#/core/applications/${this.provider.assignedApplicationSlug}">
|
return html`<a href="#/core/applications/${this.provider.assignedApplicationSlug}">
|
||||||
${this.provider.assignedApplicationName}
|
${this.provider.assignedApplicationName}
|
||||||
</a>`;
|
</a>`;
|
||||||
}
|
}
|
||||||
|
if (this.mode === "backchannel" && this.provider?.assignedBackchannelApplicationSlug) {
|
||||||
|
return html`<a
|
||||||
|
href="#/core/applications/${this.provider.assignedBackchannelApplicationSlug}"
|
||||||
|
>
|
||||||
|
${this.provider.assignedBackchannelApplicationName}
|
||||||
|
</a>`;
|
||||||
|
}
|
||||||
return html`<ak-forms-modal>
|
return html`<ak-forms-modal>
|
||||||
<span slot="submit"> ${msg("Create")} </span>
|
<span slot="submit"> ${msg("Create")} </span>
|
||||||
<span slot="header"> ${msg("Create Application")} </span>
|
<span slot="header"> ${msg("Create Application")} </span>
|
||||||
|
@ -7,10 +7,10 @@ import { EVENT_REFRESH } from "@goauthentik/common/constants";
|
|||||||
import "@goauthentik/components/events/ObjectChangelog";
|
import "@goauthentik/components/events/ObjectChangelog";
|
||||||
import { AKElement } from "@goauthentik/elements/Base";
|
import { AKElement } from "@goauthentik/elements/Base";
|
||||||
import "@goauthentik/elements/Markdown";
|
import "@goauthentik/elements/Markdown";
|
||||||
import "@goauthentik/elements/SyncStatusCard";
|
|
||||||
import "@goauthentik/elements/Tabs";
|
import "@goauthentik/elements/Tabs";
|
||||||
import "@goauthentik/elements/buttons/ActionButton";
|
import "@goauthentik/elements/buttons/ActionButton";
|
||||||
import "@goauthentik/elements/buttons/ModalButton";
|
import "@goauthentik/elements/buttons/ModalButton";
|
||||||
|
import "@goauthentik/elements/sync/SyncStatusCard";
|
||||||
|
|
||||||
import { msg } from "@lit/localize";
|
import { msg } from "@lit/localize";
|
||||||
import { CSSResult, PropertyValues, TemplateResult, html } from "lit";
|
import { CSSResult, PropertyValues, TemplateResult, html } from "lit";
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user