Compare commits
29 Commits
version/20
...
version/20
Author | SHA1 | Date | |
---|---|---|---|
8469213d82 | |||
78f7b04d5a | |||
22e586bd8c | |||
8a0b31b922 | |||
359b343f51 | |||
b727656b05 | |||
8f09c2c21c | |||
8f207c7504 | |||
34d30bb549 | |||
b4f04881e0 | |||
5314485426 | |||
ad6b6e4576 | |||
fb9aa9d7f7 | |||
fe7662f80d | |||
d6904b6aa1 | |||
cd581efacd | |||
6c159d120b | |||
4ddd4e7f88 | |||
441912414f | |||
9e177ed5c0 | |||
881548176f | |||
56739d0dc4 | |||
b23972e9c9 | |||
0a9595089e | |||
72c22b5fab | |||
84cdbb0a03 | |||
9fc659f121 | |||
db6abf61b8 | |||
6426a1d177 |
@ -1,5 +1,5 @@
|
|||||||
[bumpversion]
|
[bumpversion]
|
||||||
current_version = 2024.6.1
|
current_version = 2024.6.5
|
||||||
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*))?
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from os import environ
|
from os import environ
|
||||||
|
|
||||||
__version__ = "2024.6.1"
|
__version__ = "2024.6.5"
|
||||||
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
||||||
|
|
||||||
|
|
||||||
|
@ -14,6 +14,7 @@ from rest_framework.request import Request
|
|||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
from authentik.core.api.utils import PassiveSerializer
|
from authentik.core.api.utils import PassiveSerializer
|
||||||
|
from authentik.rbac.filters import ObjectFilter
|
||||||
|
|
||||||
|
|
||||||
class DeleteAction(Enum):
|
class DeleteAction(Enum):
|
||||||
@ -53,7 +54,7 @@ class UsedByMixin:
|
|||||||
@extend_schema(
|
@extend_schema(
|
||||||
responses={200: UsedBySerializer(many=True)},
|
responses={200: UsedBySerializer(many=True)},
|
||||||
)
|
)
|
||||||
@action(detail=True, pagination_class=None, filter_backends=[])
|
@action(detail=True, pagination_class=None, filter_backends=[ObjectFilter])
|
||||||
def used_by(self, request: Request, *args, **kwargs) -> Response:
|
def used_by(self, request: Request, *args, **kwargs) -> Response:
|
||||||
"""Get a list of all objects that use this object"""
|
"""Get a list of all objects that use this object"""
|
||||||
model: Model = self.get_object()
|
model: Model = self.get_object()
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
|
|
||||||
<html lang="en">
|
<html>
|
||||||
<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">
|
||||||
|
@ -35,6 +35,7 @@ from authentik.crypto.builder import CertificateBuilder, PrivateKeyAlg
|
|||||||
from authentik.crypto.models import CertificateKeyPair
|
from authentik.crypto.models import CertificateKeyPair
|
||||||
from authentik.events.models import Event, EventAction
|
from authentik.events.models import Event, EventAction
|
||||||
from authentik.rbac.decorators import permission_required
|
from authentik.rbac.decorators import permission_required
|
||||||
|
from authentik.rbac.filters import ObjectFilter
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
|
||||||
@ -265,7 +266,7 @@ class CertificateKeyPairViewSet(UsedByMixin, ModelViewSet):
|
|||||||
],
|
],
|
||||||
responses={200: CertificateDataSerializer(many=False)},
|
responses={200: CertificateDataSerializer(many=False)},
|
||||||
)
|
)
|
||||||
@action(detail=True, pagination_class=None, filter_backends=[])
|
@action(detail=True, pagination_class=None, filter_backends=[ObjectFilter])
|
||||||
def view_certificate(self, request: Request, pk: str) -> Response:
|
def view_certificate(self, request: Request, pk: str) -> Response:
|
||||||
"""Return certificate-key pairs certificate and log access"""
|
"""Return certificate-key pairs certificate and log access"""
|
||||||
certificate: CertificateKeyPair = self.get_object()
|
certificate: CertificateKeyPair = self.get_object()
|
||||||
@ -295,7 +296,7 @@ class CertificateKeyPairViewSet(UsedByMixin, ModelViewSet):
|
|||||||
],
|
],
|
||||||
responses={200: CertificateDataSerializer(many=False)},
|
responses={200: CertificateDataSerializer(many=False)},
|
||||||
)
|
)
|
||||||
@action(detail=True, pagination_class=None, filter_backends=[])
|
@action(detail=True, pagination_class=None, filter_backends=[ObjectFilter])
|
||||||
def view_private_key(self, request: Request, pk: str) -> Response:
|
def view_private_key(self, request: Request, pk: str) -> Response:
|
||||||
"""Return certificate-key pairs private key and log access"""
|
"""Return certificate-key pairs private key and log access"""
|
||||||
certificate: CertificateKeyPair = self.get_object()
|
certificate: CertificateKeyPair = self.get_object()
|
||||||
|
@ -214,6 +214,46 @@ class TestCrypto(APITestCase):
|
|||||||
self.assertEqual(200, response.status_code)
|
self.assertEqual(200, response.status_code)
|
||||||
self.assertIn("Content-Disposition", response)
|
self.assertIn("Content-Disposition", response)
|
||||||
|
|
||||||
|
def test_certificate_download_denied(self):
|
||||||
|
"""Test certificate export (download)"""
|
||||||
|
self.client.logout()
|
||||||
|
keypair = create_test_cert()
|
||||||
|
response = self.client.get(
|
||||||
|
reverse(
|
||||||
|
"authentik_api:certificatekeypair-view-certificate",
|
||||||
|
kwargs={"pk": keypair.pk},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.assertEqual(403, response.status_code)
|
||||||
|
response = self.client.get(
|
||||||
|
reverse(
|
||||||
|
"authentik_api:certificatekeypair-view-certificate",
|
||||||
|
kwargs={"pk": keypair.pk},
|
||||||
|
),
|
||||||
|
data={"download": True},
|
||||||
|
)
|
||||||
|
self.assertEqual(403, response.status_code)
|
||||||
|
|
||||||
|
def test_private_key_download_denied(self):
|
||||||
|
"""Test private_key export (download)"""
|
||||||
|
self.client.logout()
|
||||||
|
keypair = create_test_cert()
|
||||||
|
response = self.client.get(
|
||||||
|
reverse(
|
||||||
|
"authentik_api:certificatekeypair-view-private-key",
|
||||||
|
kwargs={"pk": keypair.pk},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.assertEqual(403, response.status_code)
|
||||||
|
response = self.client.get(
|
||||||
|
reverse(
|
||||||
|
"authentik_api:certificatekeypair-view-private-key",
|
||||||
|
kwargs={"pk": keypair.pk},
|
||||||
|
),
|
||||||
|
data={"download": True},
|
||||||
|
)
|
||||||
|
self.assertEqual(403, response.status_code)
|
||||||
|
|
||||||
def test_used_by(self):
|
def test_used_by(self):
|
||||||
"""Test used_by endpoint"""
|
"""Test used_by endpoint"""
|
||||||
self.client.force_login(create_test_admin_user())
|
self.client.force_login(create_test_admin_user())
|
||||||
@ -246,6 +286,26 @@ class TestCrypto(APITestCase):
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_used_by_denied(self):
|
||||||
|
"""Test used_by endpoint"""
|
||||||
|
self.client.logout()
|
||||||
|
keypair = create_test_cert()
|
||||||
|
OAuth2Provider.objects.create(
|
||||||
|
name=generate_id(),
|
||||||
|
client_id="test",
|
||||||
|
client_secret=generate_key(),
|
||||||
|
authorization_flow=create_test_flow(),
|
||||||
|
redirect_uris="http://localhost",
|
||||||
|
signing_key=keypair,
|
||||||
|
)
|
||||||
|
response = self.client.get(
|
||||||
|
reverse(
|
||||||
|
"authentik_api:certificatekeypair-used-by",
|
||||||
|
kwargs={"pk": keypair.pk},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.assertEqual(403, response.status_code)
|
||||||
|
|
||||||
def test_discovery(self):
|
def test_discovery(self):
|
||||||
"""Test certificate discovery"""
|
"""Test certificate discovery"""
|
||||||
name = generate_id()
|
name = generate_id()
|
||||||
|
@ -34,6 +34,12 @@ class ConnectionTokenSerializer(EnterpriseRequiredMixin, ModelSerializer):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class ConnectionTokenOwnerFilter(OwnerFilter):
|
||||||
|
"""Owner filter for connection tokens (checks session's user)"""
|
||||||
|
|
||||||
|
owner_key = "session__user"
|
||||||
|
|
||||||
|
|
||||||
class ConnectionTokenViewSet(
|
class ConnectionTokenViewSet(
|
||||||
mixins.RetrieveModelMixin,
|
mixins.RetrieveModelMixin,
|
||||||
mixins.UpdateModelMixin,
|
mixins.UpdateModelMixin,
|
||||||
@ -50,4 +56,9 @@ class ConnectionTokenViewSet(
|
|||||||
search_fields = ["endpoint__name", "provider__name"]
|
search_fields = ["endpoint__name", "provider__name"]
|
||||||
ordering = ["endpoint__name", "provider__name"]
|
ordering = ["endpoint__name", "provider__name"]
|
||||||
permission_classes = [OwnerSuperuserPermissions]
|
permission_classes = [OwnerSuperuserPermissions]
|
||||||
filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter]
|
filter_backends = [
|
||||||
|
ConnectionTokenOwnerFilter,
|
||||||
|
DjangoFilterBackend,
|
||||||
|
OrderingFilter,
|
||||||
|
SearchFilter,
|
||||||
|
]
|
||||||
|
@ -5,7 +5,6 @@ from channels.sessions import CookieMiddleware
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
from django.views.decorators.csrf import ensure_csrf_cookie
|
from django.views.decorators.csrf import ensure_csrf_cookie
|
||||||
|
|
||||||
from authentik.core.channels import TokenOutpostMiddleware
|
|
||||||
from authentik.enterprise.providers.rac.api.connection_tokens import ConnectionTokenViewSet
|
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.endpoints import EndpointViewSet
|
||||||
from authentik.enterprise.providers.rac.api.property_mappings import RACPropertyMappingViewSet
|
from authentik.enterprise.providers.rac.api.property_mappings import RACPropertyMappingViewSet
|
||||||
@ -13,6 +12,7 @@ from authentik.enterprise.providers.rac.api.providers import RACProviderViewSet
|
|||||||
from authentik.enterprise.providers.rac.consumer_client import RACClientConsumer
|
from authentik.enterprise.providers.rac.consumer_client import RACClientConsumer
|
||||||
from authentik.enterprise.providers.rac.consumer_outpost import RACOutpostConsumer
|
from authentik.enterprise.providers.rac.consumer_outpost import RACOutpostConsumer
|
||||||
from authentik.enterprise.providers.rac.views import RACInterface, RACStartView
|
from authentik.enterprise.providers.rac.views import RACInterface, RACStartView
|
||||||
|
from authentik.outposts.channels import TokenOutpostMiddleware
|
||||||
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
|
||||||
|
|
||||||
|
@ -35,6 +35,7 @@ IGNORED_MODELS = tuple(
|
|||||||
|
|
||||||
_CTX_OVERWRITE_USER = ContextVar[User | None]("authentik_events_log_overwrite_user", default=None)
|
_CTX_OVERWRITE_USER = ContextVar[User | None]("authentik_events_log_overwrite_user", default=None)
|
||||||
_CTX_IGNORE = ContextVar[bool]("authentik_events_log_ignore", default=False)
|
_CTX_IGNORE = ContextVar[bool]("authentik_events_log_ignore", default=False)
|
||||||
|
_CTX_REQUEST = ContextVar[HttpRequest | None]("authentik_events_log_request", default=None)
|
||||||
|
|
||||||
|
|
||||||
def should_log_model(model: Model) -> bool:
|
def should_log_model(model: Model) -> bool:
|
||||||
@ -149,11 +150,13 @@ class AuditMiddleware:
|
|||||||
m2m_changed.disconnect(dispatch_uid=request.request_id)
|
m2m_changed.disconnect(dispatch_uid=request.request_id)
|
||||||
|
|
||||||
def __call__(self, request: HttpRequest) -> HttpResponse:
|
def __call__(self, request: HttpRequest) -> HttpResponse:
|
||||||
|
_CTX_REQUEST.set(request)
|
||||||
self.connect(request)
|
self.connect(request)
|
||||||
|
|
||||||
response = self.get_response(request)
|
response = self.get_response(request)
|
||||||
|
|
||||||
self.disconnect(request)
|
self.disconnect(request)
|
||||||
|
_CTX_REQUEST.set(None)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def process_exception(self, request: HttpRequest, exception: Exception):
|
def process_exception(self, request: HttpRequest, exception: Exception):
|
||||||
@ -167,7 +170,7 @@ class AuditMiddleware:
|
|||||||
thread = EventNewThread(
|
thread = EventNewThread(
|
||||||
EventAction.SUSPICIOUS_REQUEST,
|
EventAction.SUSPICIOUS_REQUEST,
|
||||||
request,
|
request,
|
||||||
message=str(exception),
|
message=exception_to_string(exception),
|
||||||
)
|
)
|
||||||
thread.run()
|
thread.run()
|
||||||
elif before_send({}, {"exc_info": (None, exception, None)}) is not None:
|
elif before_send({}, {"exc_info": (None, exception, None)}) is not None:
|
||||||
@ -192,6 +195,8 @@ class AuditMiddleware:
|
|||||||
return
|
return
|
||||||
if _CTX_IGNORE.get():
|
if _CTX_IGNORE.get():
|
||||||
return
|
return
|
||||||
|
if request.request_id != _CTX_REQUEST.get().request_id:
|
||||||
|
return
|
||||||
user = self.get_user(request)
|
user = self.get_user(request)
|
||||||
|
|
||||||
action = EventAction.MODEL_CREATED if created else EventAction.MODEL_UPDATED
|
action = EventAction.MODEL_CREATED if created else EventAction.MODEL_UPDATED
|
||||||
@ -205,6 +210,8 @@ class AuditMiddleware:
|
|||||||
return
|
return
|
||||||
if _CTX_IGNORE.get():
|
if _CTX_IGNORE.get():
|
||||||
return
|
return
|
||||||
|
if request.request_id != _CTX_REQUEST.get().request_id:
|
||||||
|
return
|
||||||
user = self.get_user(request)
|
user = self.get_user(request)
|
||||||
|
|
||||||
EventNewThread(
|
EventNewThread(
|
||||||
@ -230,6 +237,8 @@ class AuditMiddleware:
|
|||||||
return
|
return
|
||||||
if _CTX_IGNORE.get():
|
if _CTX_IGNORE.get():
|
||||||
return
|
return
|
||||||
|
if request.request_id != _CTX_REQUEST.get().request_id:
|
||||||
|
return
|
||||||
user = self.get_user(request)
|
user = self.get_user(request)
|
||||||
|
|
||||||
EventNewThread(
|
EventNewThread(
|
||||||
|
@ -238,6 +238,8 @@ class Event(SerializerModel, ExpiringModel):
|
|||||||
"args": cleanse_dict(QueryDict(request.META.get("QUERY_STRING", ""))),
|
"args": cleanse_dict(QueryDict(request.META.get("QUERY_STRING", ""))),
|
||||||
"user_agent": request.META.get("HTTP_USER_AGENT", ""),
|
"user_agent": request.META.get("HTTP_USER_AGENT", ""),
|
||||||
}
|
}
|
||||||
|
if hasattr(request, "request_id"):
|
||||||
|
self.context["http_request"]["request_id"] = request.request_id
|
||||||
# Special case for events created during flow execution
|
# Special case for events created during flow execution
|
||||||
# since they keep the http query within a wrapped query
|
# since they keep the http query within a wrapped query
|
||||||
if QS_QUERY in self.context["http_request"]["args"]:
|
if QS_QUERY in self.context["http_request"]["args"]:
|
||||||
|
@ -75,7 +75,10 @@ def on_login_failed(
|
|||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
"""Failed Login, authentik custom event"""
|
"""Failed Login, authentik custom event"""
|
||||||
Event.new(EventAction.LOGIN_FAILED, **credentials, stage=stage, **kwargs).from_http(request)
|
user = User.objects.filter(username=credentials.get("username")).first()
|
||||||
|
Event.new(EventAction.LOGIN_FAILED, **credentials, stage=stage, **kwargs).from_http(
|
||||||
|
request, user
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@receiver(invitation_used)
|
@receiver(invitation_used)
|
||||||
|
@ -37,6 +37,7 @@ from authentik.lib.utils.file import (
|
|||||||
)
|
)
|
||||||
from authentik.lib.views import bad_request_message
|
from authentik.lib.views import bad_request_message
|
||||||
from authentik.rbac.decorators import permission_required
|
from authentik.rbac.decorators import permission_required
|
||||||
|
from authentik.rbac.filters import ObjectFilter
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
|
||||||
@ -281,7 +282,7 @@ class FlowViewSet(UsedByMixin, ModelViewSet):
|
|||||||
400: OpenApiResponse(description="Flow not applicable"),
|
400: OpenApiResponse(description="Flow not applicable"),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@action(detail=True, pagination_class=None, filter_backends=[])
|
@action(detail=True, pagination_class=None, filter_backends=[ObjectFilter])
|
||||||
def execute(self, request: Request, slug: str):
|
def execute(self, request: Request, slug: str):
|
||||||
"""Execute flow for current user"""
|
"""Execute flow for current user"""
|
||||||
# Because we pre-plan the flow here, and not in the planner, we need to manually clear
|
# Because we pre-plan the flow here, and not in the planner, we need to manually clear
|
||||||
|
@ -229,6 +229,8 @@ class SyncTasks:
|
|||||||
client.delete(instance)
|
client.delete(instance)
|
||||||
except TransientSyncException as exc:
|
except TransientSyncException as exc:
|
||||||
raise Retry() from exc
|
raise Retry() from exc
|
||||||
|
except SkipObjectException:
|
||||||
|
continue
|
||||||
except StopSync as exc:
|
except StopSync as exc:
|
||||||
self.logger.warning(exc, provider_pk=provider.pk)
|
self.logger.warning(exc, provider_pk=provider.pk)
|
||||||
|
|
||||||
@ -259,5 +261,7 @@ class SyncTasks:
|
|||||||
client.update_group(group, operation, pk_set)
|
client.update_group(group, operation, pk_set)
|
||||||
except TransientSyncException as exc:
|
except TransientSyncException as exc:
|
||||||
raise Retry() from exc
|
raise Retry() from exc
|
||||||
|
except SkipObjectException:
|
||||||
|
continue
|
||||||
except StopSync as exc:
|
except StopSync as exc:
|
||||||
self.logger.warning(exc, provider_pk=provider.pk)
|
self.logger.warning(exc, provider_pk=provider.pk)
|
||||||
|
@ -30,6 +30,11 @@ class TestHTTP(TestCase):
|
|||||||
request = self.factory.get("/", HTTP_X_FORWARDED_FOR="127.0.0.2")
|
request = self.factory.get("/", HTTP_X_FORWARDED_FOR="127.0.0.2")
|
||||||
self.assertEqual(ClientIPMiddleware.get_client_ip(request), "127.0.0.2")
|
self.assertEqual(ClientIPMiddleware.get_client_ip(request), "127.0.0.2")
|
||||||
|
|
||||||
|
def test_forward_for_invalid(self):
|
||||||
|
"""Test invalid forward for"""
|
||||||
|
request = self.factory.get("/", HTTP_X_FORWARDED_FOR="foobar")
|
||||||
|
self.assertEqual(ClientIPMiddleware.get_client_ip(request), ClientIPMiddleware.default_ip)
|
||||||
|
|
||||||
def test_fake_outpost(self):
|
def test_fake_outpost(self):
|
||||||
"""Test faked IP which is overridden by an outpost"""
|
"""Test faked IP which is overridden by an outpost"""
|
||||||
token = Token.objects.create(
|
token = Token.objects.create(
|
||||||
@ -53,6 +58,17 @@ class TestHTTP(TestCase):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
self.assertEqual(ClientIPMiddleware.get_client_ip(request), "127.0.0.1")
|
self.assertEqual(ClientIPMiddleware.get_client_ip(request), "127.0.0.1")
|
||||||
|
# Invalid, not a real IP
|
||||||
|
self.user.type = UserTypes.INTERNAL_SERVICE_ACCOUNT
|
||||||
|
self.user.save()
|
||||||
|
request = self.factory.get(
|
||||||
|
"/",
|
||||||
|
**{
|
||||||
|
ClientIPMiddleware.outpost_remote_ip_header: "foobar",
|
||||||
|
ClientIPMiddleware.outpost_token_header: token.key,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(ClientIPMiddleware.get_client_ip(request), "127.0.0.1")
|
||||||
# Valid
|
# Valid
|
||||||
self.user.type = UserTypes.INTERNAL_SERVICE_ACCOUNT
|
self.user.type = UserTypes.INTERNAL_SERVICE_ACCOUNT
|
||||||
self.user.save()
|
self.user.save()
|
||||||
|
@ -20,6 +20,7 @@ from authentik.core.api.utils import JSONDictField, ModelSerializer, PassiveSeri
|
|||||||
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.enterprise.providers.rac.models import RACProvider
|
||||||
|
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
|
||||||
from authentik.outposts.models import (
|
from authentik.outposts.models import (
|
||||||
@ -49,6 +50,10 @@ class OutpostSerializer(ModelSerializer):
|
|||||||
service_connection_obj = ServiceConnectionSerializer(
|
service_connection_obj = ServiceConnectionSerializer(
|
||||||
source="service_connection", read_only=True
|
source="service_connection", read_only=True
|
||||||
)
|
)
|
||||||
|
refresh_interval_s = SerializerMethodField()
|
||||||
|
|
||||||
|
def get_refresh_interval_s(self, obj: Outpost) -> int:
|
||||||
|
return int(timedelta_from_string(obj.config.refresh_interval).total_seconds())
|
||||||
|
|
||||||
def validate_name(self, name: str) -> str:
|
def validate_name(self, name: str) -> str:
|
||||||
"""Validate name (especially for embedded outpost)"""
|
"""Validate name (especially for embedded outpost)"""
|
||||||
@ -84,7 +89,8 @@ class OutpostSerializer(ModelSerializer):
|
|||||||
def validate_config(self, config) -> dict:
|
def validate_config(self, config) -> dict:
|
||||||
"""Check that the config has all required fields"""
|
"""Check that the config has all required fields"""
|
||||||
try:
|
try:
|
||||||
from_dict(OutpostConfig, config)
|
parsed = from_dict(OutpostConfig, config)
|
||||||
|
timedelta_string_validator(parsed.refresh_interval)
|
||||||
except DaciteError as exc:
|
except DaciteError as exc:
|
||||||
raise ValidationError(f"Failed to validate config: {str(exc)}") from exc
|
raise ValidationError(f"Failed to validate config: {str(exc)}") from exc
|
||||||
return config
|
return config
|
||||||
@ -99,6 +105,7 @@ class OutpostSerializer(ModelSerializer):
|
|||||||
"providers_obj",
|
"providers_obj",
|
||||||
"service_connection",
|
"service_connection",
|
||||||
"service_connection_obj",
|
"service_connection_obj",
|
||||||
|
"refresh_interval_s",
|
||||||
"token_identifier",
|
"token_identifier",
|
||||||
"config",
|
"config",
|
||||||
"managed",
|
"managed",
|
||||||
|
@ -26,6 +26,7 @@ from authentik.outposts.models import (
|
|||||||
KubernetesServiceConnection,
|
KubernetesServiceConnection,
|
||||||
OutpostServiceConnection,
|
OutpostServiceConnection,
|
||||||
)
|
)
|
||||||
|
from authentik.rbac.filters import ObjectFilter
|
||||||
|
|
||||||
|
|
||||||
class ServiceConnectionSerializer(ModelSerializer, MetaNameSerializer):
|
class ServiceConnectionSerializer(ModelSerializer, MetaNameSerializer):
|
||||||
@ -75,7 +76,7 @@ class ServiceConnectionViewSet(
|
|||||||
filterset_fields = ["name"]
|
filterset_fields = ["name"]
|
||||||
|
|
||||||
@extend_schema(responses={200: ServiceConnectionStateSerializer(many=False)})
|
@extend_schema(responses={200: ServiceConnectionStateSerializer(many=False)})
|
||||||
@action(detail=True, pagination_class=None, filter_backends=[])
|
@action(detail=True, pagination_class=None, filter_backends=[ObjectFilter])
|
||||||
def state(self, request: Request, pk: str) -> Response:
|
def state(self, request: Request, pk: str) -> Response:
|
||||||
"""Get the service connection's state"""
|
"""Get the service connection's state"""
|
||||||
connection = self.get_object()
|
connection = self.get_object()
|
||||||
|
@ -61,6 +61,7 @@ class OutpostConfig:
|
|||||||
|
|
||||||
log_level: str = CONFIG.get("log_level")
|
log_level: str = CONFIG.get("log_level")
|
||||||
object_naming_template: str = field(default="ak-outpost-%(name)s")
|
object_naming_template: str = field(default="ak-outpost-%(name)s")
|
||||||
|
refresh_interval: str = "minutes=5"
|
||||||
|
|
||||||
container_image: str | None = field(default=None)
|
container_image: str | None = field(default=None)
|
||||||
|
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
from dataclasses import asdict
|
from dataclasses import asdict
|
||||||
|
|
||||||
from channels.exceptions import DenyConnection
|
|
||||||
from channels.routing import URLRouter
|
from channels.routing import URLRouter
|
||||||
from channels.testing import WebsocketCommunicator
|
from channels.testing import WebsocketCommunicator
|
||||||
from django.test import TransactionTestCase
|
from django.test import TransactionTestCase
|
||||||
@ -37,9 +36,8 @@ class TestOutpostWS(TransactionTestCase):
|
|||||||
communicator = WebsocketCommunicator(
|
communicator = WebsocketCommunicator(
|
||||||
URLRouter(websocket.websocket_urlpatterns), f"/ws/outpost/{self.outpost.pk}/"
|
URLRouter(websocket.websocket_urlpatterns), f"/ws/outpost/{self.outpost.pk}/"
|
||||||
)
|
)
|
||||||
with self.assertRaises(DenyConnection):
|
connected, _ = await communicator.connect()
|
||||||
connected, _ = await communicator.connect()
|
self.assertFalse(connected)
|
||||||
self.assertFalse(connected)
|
|
||||||
|
|
||||||
async def test_auth_valid(self):
|
async def test_auth_valid(self):
|
||||||
"""Test auth with token"""
|
"""Test auth with token"""
|
||||||
|
@ -2,13 +2,13 @@
|
|||||||
|
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from authentik.core.channels import TokenOutpostMiddleware
|
|
||||||
from authentik.outposts.api.outposts import OutpostViewSet
|
from authentik.outposts.api.outposts import OutpostViewSet
|
||||||
from authentik.outposts.api.service_connections import (
|
from authentik.outposts.api.service_connections import (
|
||||||
DockerServiceConnectionViewSet,
|
DockerServiceConnectionViewSet,
|
||||||
KubernetesServiceConnectionViewSet,
|
KubernetesServiceConnectionViewSet,
|
||||||
ServiceConnectionViewSet,
|
ServiceConnectionViewSet,
|
||||||
)
|
)
|
||||||
|
from authentik.outposts.channels import TokenOutpostMiddleware
|
||||||
from authentik.outposts.consumer import OutpostConsumer
|
from authentik.outposts.consumer import OutpostConsumer
|
||||||
from authentik.root.middleware import ChannelsLoggingMiddleware
|
from authentik.root.middleware import ChannelsLoggingMiddleware
|
||||||
|
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
"""Reputation policy API Views"""
|
"""Reputation policy API Views"""
|
||||||
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from django_filters.filters import BaseInFilter, CharFilter
|
||||||
|
from django_filters.filterset import FilterSet
|
||||||
from rest_framework import mixins
|
from rest_framework import mixins
|
||||||
from rest_framework.exceptions import ValidationError
|
from rest_framework.exceptions import ValidationError
|
||||||
from rest_framework.viewsets import GenericViewSet, ModelViewSet
|
from rest_framework.viewsets import GenericViewSet, ModelViewSet
|
||||||
@ -11,6 +13,10 @@ from authentik.policies.api.policies import PolicySerializer
|
|||||||
from authentik.policies.reputation.models import Reputation, ReputationPolicy
|
from authentik.policies.reputation.models import Reputation, ReputationPolicy
|
||||||
|
|
||||||
|
|
||||||
|
class CharInFilter(BaseInFilter, CharFilter):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ReputationPolicySerializer(PolicySerializer):
|
class ReputationPolicySerializer(PolicySerializer):
|
||||||
"""Reputation Policy Serializer"""
|
"""Reputation Policy Serializer"""
|
||||||
|
|
||||||
@ -38,6 +44,16 @@ class ReputationPolicyViewSet(UsedByMixin, ModelViewSet):
|
|||||||
ordering = ["name"]
|
ordering = ["name"]
|
||||||
|
|
||||||
|
|
||||||
|
class ReputationFilter(FilterSet):
|
||||||
|
"""Filter for reputation"""
|
||||||
|
|
||||||
|
identifier_in = CharInFilter(field_name="identifier", lookup_expr="in")
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Reputation
|
||||||
|
fields = ["identifier", "ip", "score"]
|
||||||
|
|
||||||
|
|
||||||
class ReputationSerializer(ModelSerializer):
|
class ReputationSerializer(ModelSerializer):
|
||||||
"""Reputation Serializer"""
|
"""Reputation Serializer"""
|
||||||
|
|
||||||
@ -66,5 +82,5 @@ class ReputationViewSet(
|
|||||||
queryset = Reputation.objects.all()
|
queryset = Reputation.objects.all()
|
||||||
serializer_class = ReputationSerializer
|
serializer_class = ReputationSerializer
|
||||||
search_fields = ["identifier", "ip", "score"]
|
search_fields = ["identifier", "ip", "score"]
|
||||||
filterset_fields = ["identifier", "ip", "score"]
|
filterset_class = ReputationFilter
|
||||||
ordering = ["ip"]
|
ordering = ["ip"]
|
||||||
|
@ -29,7 +29,6 @@ class TesOAuth2Introspection(OAuthTestCase):
|
|||||||
self.app = Application.objects.create(
|
self.app = Application.objects.create(
|
||||||
name=generate_id(), slug=generate_id(), provider=self.provider
|
name=generate_id(), slug=generate_id(), provider=self.provider
|
||||||
)
|
)
|
||||||
self.app.save()
|
|
||||||
self.user = create_test_admin_user()
|
self.user = create_test_admin_user()
|
||||||
self.auth = b64encode(
|
self.auth = b64encode(
|
||||||
f"{self.provider.client_id}:{self.provider.client_secret}".encode()
|
f"{self.provider.client_id}:{self.provider.client_secret}".encode()
|
||||||
@ -114,6 +113,41 @@ class TesOAuth2Introspection(OAuthTestCase):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_introspect_invalid_provider(self):
|
||||||
|
"""Test introspection (mismatched provider and token)"""
|
||||||
|
provider: OAuth2Provider = OAuth2Provider.objects.create(
|
||||||
|
name=generate_id(),
|
||||||
|
authorization_flow=create_test_flow(),
|
||||||
|
redirect_uris="",
|
||||||
|
signing_key=create_test_cert(),
|
||||||
|
)
|
||||||
|
auth = b64encode(f"{provider.client_id}:{provider.client_secret}".encode()).decode()
|
||||||
|
|
||||||
|
token: AccessToken = AccessToken.objects.create(
|
||||||
|
provider=self.provider,
|
||||||
|
user=self.user,
|
||||||
|
token=generate_id(),
|
||||||
|
auth_time=timezone.now(),
|
||||||
|
_scope="openid user profile",
|
||||||
|
_id_token=json.dumps(
|
||||||
|
asdict(
|
||||||
|
IDToken("foo", "bar"),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
res = self.client.post(
|
||||||
|
reverse("authentik_providers_oauth2:token-introspection"),
|
||||||
|
HTTP_AUTHORIZATION=f"Basic {auth}",
|
||||||
|
data={"token": token.token},
|
||||||
|
)
|
||||||
|
self.assertEqual(res.status_code, 200)
|
||||||
|
self.assertJSONEqual(
|
||||||
|
res.content.decode(),
|
||||||
|
{
|
||||||
|
"active": False,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
def test_introspect_invalid_auth(self):
|
def test_introspect_invalid_auth(self):
|
||||||
"""Test introspect (invalid auth)"""
|
"""Test introspect (invalid auth)"""
|
||||||
res = self.client.post(
|
res = self.client.post(
|
||||||
|
@ -46,10 +46,10 @@ class TokenIntrospectionParams:
|
|||||||
if not provider:
|
if not provider:
|
||||||
raise TokenIntrospectionError
|
raise TokenIntrospectionError
|
||||||
|
|
||||||
access_token = AccessToken.objects.filter(token=raw_token).first()
|
access_token = AccessToken.objects.filter(token=raw_token, provider=provider).first()
|
||||||
if access_token:
|
if access_token:
|
||||||
return TokenIntrospectionParams(access_token, provider)
|
return TokenIntrospectionParams(access_token, provider)
|
||||||
refresh_token = RefreshToken.objects.filter(token=raw_token).first()
|
refresh_token = RefreshToken.objects.filter(token=raw_token, provider=provider).first()
|
||||||
if refresh_token:
|
if refresh_token:
|
||||||
return TokenIntrospectionParams(refresh_token, provider)
|
return TokenIntrospectionParams(refresh_token, provider)
|
||||||
LOGGER.debug("Token does not exist", token=raw_token)
|
LOGGER.debug("Token does not exist", token=raw_token)
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from hashlib import sha512
|
from hashlib import sha512
|
||||||
|
from ipaddress import ip_address
|
||||||
from time import perf_counter, time
|
from time import perf_counter, time
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
@ -174,6 +175,7 @@ class ClientIPMiddleware:
|
|||||||
|
|
||||||
def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]):
|
def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]):
|
||||||
self.get_response = get_response
|
self.get_response = get_response
|
||||||
|
self.logger = get_logger().bind()
|
||||||
|
|
||||||
def _get_client_ip_from_meta(self, meta: dict[str, Any]) -> str:
|
def _get_client_ip_from_meta(self, meta: dict[str, Any]) -> str:
|
||||||
"""Attempt to get the client's IP by checking common HTTP Headers.
|
"""Attempt to get the client's IP by checking common HTTP Headers.
|
||||||
@ -185,11 +187,16 @@ class ClientIPMiddleware:
|
|||||||
"HTTP_X_FORWARDED_FOR",
|
"HTTP_X_FORWARDED_FOR",
|
||||||
"REMOTE_ADDR",
|
"REMOTE_ADDR",
|
||||||
)
|
)
|
||||||
for _header in headers:
|
try:
|
||||||
if _header in meta:
|
for _header in headers:
|
||||||
ips: list[str] = meta.get(_header).split(",")
|
if _header in meta:
|
||||||
return ips[0].strip()
|
ips: list[str] = meta.get(_header).split(",")
|
||||||
return self.default_ip
|
# Ensure the IP parses as a valid IP
|
||||||
|
return str(ip_address(ips[0].strip()))
|
||||||
|
return self.default_ip
|
||||||
|
except ValueError as exc:
|
||||||
|
self.logger.debug("Invalid remote IP", exc=exc)
|
||||||
|
return self.default_ip
|
||||||
|
|
||||||
# FIXME: this should probably not be in `root` but rather in a middleware in `outposts`
|
# FIXME: this should probably not be in `root` but rather in a middleware in `outposts`
|
||||||
# but for now it's fine
|
# but for now it's fine
|
||||||
@ -228,7 +235,11 @@ class ClientIPMiddleware:
|
|||||||
Hub.current.scope.set_user(user)
|
Hub.current.scope.set_user(user)
|
||||||
# Set the outpost service account on the request
|
# Set the outpost service account on the request
|
||||||
setattr(request, self.request_attr_outpost_user, user)
|
setattr(request, self.request_attr_outpost_user, user)
|
||||||
return delegated_ip
|
try:
|
||||||
|
return str(ip_address(delegated_ip))
|
||||||
|
except ValueError as exc:
|
||||||
|
self.logger.debug("Invalid remote IP from Outpost", exc=exc)
|
||||||
|
return None
|
||||||
|
|
||||||
def _get_client_ip(self, request: HttpRequest | None) -> str:
|
def _get_client_ip(self, request: HttpRequest | None) -> str:
|
||||||
"""Attempt to get the client's IP by checking common HTTP Headers.
|
"""Attempt to get the client's IP by checking common HTTP Headers.
|
||||||
@ -274,9 +285,13 @@ class ChannelsLoggingMiddleware:
|
|||||||
self.log(scope)
|
self.log(scope)
|
||||||
try:
|
try:
|
||||||
return await self.inner(scope, receive, send)
|
return await self.inner(scope, receive, send)
|
||||||
|
except DenyConnection:
|
||||||
|
return await send({"type": "websocket.close"})
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
|
if settings.DEBUG:
|
||||||
|
raise exc
|
||||||
LOGGER.warning("Exception in ASGI application", exc=exc)
|
LOGGER.warning("Exception in ASGI application", exc=exc)
|
||||||
raise DenyConnection() from None
|
return await send({"type": "websocket.close"})
|
||||||
|
|
||||||
def log(self, scope: dict, **kwargs):
|
def log(self, scope: dict, **kwargs):
|
||||||
"""Log request"""
|
"""Log request"""
|
||||||
|
@ -39,7 +39,7 @@ def sync_ldap_source_on_save(sender, instance: LDAPSource, **_):
|
|||||||
@receiver(password_validate)
|
@receiver(password_validate)
|
||||||
def ldap_password_validate(sender, password: str, plan_context: dict[str, Any], **__):
|
def ldap_password_validate(sender, password: str, plan_context: dict[str, Any], **__):
|
||||||
"""if there's an LDAP Source with enabled password sync, check the password"""
|
"""if there's an LDAP Source with enabled password sync, check the password"""
|
||||||
sources = LDAPSource.objects.filter(sync_users_password=True)
|
sources = LDAPSource.objects.filter(sync_users_password=True, enabled=True)
|
||||||
if not sources.exists():
|
if not sources.exists():
|
||||||
return
|
return
|
||||||
source = sources.first()
|
source = sources.first()
|
||||||
@ -56,7 +56,7 @@ def ldap_password_validate(sender, password: str, plan_context: dict[str, Any],
|
|||||||
@receiver(password_changed)
|
@receiver(password_changed)
|
||||||
def ldap_sync_password(sender, user: User, password: str, **_):
|
def ldap_sync_password(sender, user: User, password: str, **_):
|
||||||
"""Connect to ldap and update password."""
|
"""Connect to ldap and update password."""
|
||||||
sources = LDAPSource.objects.filter(sync_users_password=True)
|
sources = LDAPSource.objects.filter(sync_users_password=True, enabled=True)
|
||||||
if not sources.exists():
|
if not sources.exists():
|
||||||
return
|
return
|
||||||
source = sources.first()
|
source = sources.first()
|
||||||
|
@ -2,9 +2,6 @@
|
|||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from facebook import GraphAPI
|
|
||||||
|
|
||||||
from authentik.sources.oauth.clients.oauth2 import OAuth2Client
|
|
||||||
from authentik.sources.oauth.types.registry import SourceType, registry
|
from authentik.sources.oauth.types.registry import SourceType, registry
|
||||||
from authentik.sources.oauth.views.callback import OAuthCallback
|
from authentik.sources.oauth.views.callback import OAuthCallback
|
||||||
from authentik.sources.oauth.views.redirect import OAuthRedirect
|
from authentik.sources.oauth.views.redirect import OAuthRedirect
|
||||||
@ -19,19 +16,9 @@ class FacebookOAuthRedirect(OAuthRedirect):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class FacebookOAuth2Client(OAuth2Client):
|
|
||||||
"""Facebook OAuth2 Client"""
|
|
||||||
|
|
||||||
def get_profile_info(self, token: dict[str, str]) -> dict[str, Any] | None:
|
|
||||||
api = GraphAPI(access_token=token["access_token"])
|
|
||||||
return api.get_object("me", fields="id,name,email")
|
|
||||||
|
|
||||||
|
|
||||||
class FacebookOAuth2Callback(OAuthCallback):
|
class FacebookOAuth2Callback(OAuthCallback):
|
||||||
"""Facebook OAuth2 Callback"""
|
"""Facebook OAuth2 Callback"""
|
||||||
|
|
||||||
client_class = FacebookOAuth2Client
|
|
||||||
|
|
||||||
def get_user_enroll_context(
|
def get_user_enroll_context(
|
||||||
self,
|
self,
|
||||||
info: dict[str, Any],
|
info: dict[str, Any],
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
"""SCIM Source"""
|
"""SCIM Source"""
|
||||||
|
|
||||||
from uuid import uuid4
|
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.templatetags.static import static
|
from django.templatetags.static import static
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
@ -19,8 +17,6 @@ class SCIMSource(Source):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def service_account_identifier(self) -> str:
|
def service_account_identifier(self) -> str:
|
||||||
if not self.pk:
|
|
||||||
self.pk = uuid4()
|
|
||||||
return f"ak-source-scim-{self.pk}"
|
return f"ak-source-scim-{self.pk}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -1,41 +1,44 @@
|
|||||||
from django.db.models import Model
|
from django.db.models import Model
|
||||||
from django.db.models.signals import pre_delete, pre_save
|
from django.db.models.signals import post_delete, post_save
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
|
|
||||||
from authentik.core.models import USER_PATH_SYSTEM_PREFIX, Token, TokenIntents, User, UserTypes
|
from authentik.core.models import USER_PATH_SYSTEM_PREFIX, Token, TokenIntents, User, UserTypes
|
||||||
|
from authentik.events.middleware import audit_ignore
|
||||||
from authentik.sources.scim.models import SCIMSource
|
from authentik.sources.scim.models import SCIMSource
|
||||||
|
|
||||||
USER_PATH_SOURCE_SCIM = USER_PATH_SYSTEM_PREFIX + "/sources/scim"
|
USER_PATH_SOURCE_SCIM = USER_PATH_SYSTEM_PREFIX + "/sources/scim"
|
||||||
|
|
||||||
|
|
||||||
@receiver(pre_save, sender=SCIMSource)
|
@receiver(post_save, sender=SCIMSource)
|
||||||
def scim_source_pre_save(sender: type[Model], instance: SCIMSource, **_):
|
def scim_source_post_save(sender: type[Model], instance: SCIMSource, created: bool, **_):
|
||||||
"""Create service account before source is saved"""
|
"""Create service account before source is saved"""
|
||||||
# .service_account_identifier will auto-assign a primary key uuid to the source
|
|
||||||
# if none is set yet, just so we can get the identifier before we save
|
|
||||||
identifier = instance.service_account_identifier
|
identifier = instance.service_account_identifier
|
||||||
user = User.objects.create(
|
user, _ = User.objects.update_or_create(
|
||||||
username=identifier,
|
username=identifier,
|
||||||
name=f"SCIM Source {instance.name} Service-Account",
|
defaults={
|
||||||
type=UserTypes.INTERNAL_SERVICE_ACCOUNT,
|
"name": f"SCIM Source {instance.name} Service-Account",
|
||||||
path=USER_PATH_SOURCE_SCIM,
|
"type": UserTypes.INTERNAL_SERVICE_ACCOUNT,
|
||||||
|
"path": USER_PATH_SOURCE_SCIM,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
token = Token.objects.create(
|
token, token_created = Token.objects.update_or_create(
|
||||||
user=user,
|
|
||||||
identifier=identifier,
|
identifier=identifier,
|
||||||
intent=TokenIntents.INTENT_API,
|
defaults={
|
||||||
expiring=False,
|
"user": user,
|
||||||
managed=f"goauthentik.io/sources/scim/{instance.pk}",
|
"intent": TokenIntents.INTENT_API,
|
||||||
|
"expiring": False,
|
||||||
|
"managed": f"goauthentik.io/sources/scim/{instance.pk}",
|
||||||
|
},
|
||||||
)
|
)
|
||||||
instance.token = token
|
if created or token_created:
|
||||||
|
with audit_ignore():
|
||||||
|
instance.token = token
|
||||||
|
instance.save()
|
||||||
|
|
||||||
|
|
||||||
@receiver(pre_delete, sender=SCIMSource)
|
@receiver(post_delete, sender=SCIMSource)
|
||||||
def scim_source_pre_delete(sender: type[Model], instance: SCIMSource, **_):
|
def scim_source_post_delete(sender: type[Model], instance: SCIMSource, **_):
|
||||||
"""Delete SCIM Source service account before deleting source"""
|
"""Delete SCIM Source service account after deleting source"""
|
||||||
Token.objects.filter(
|
|
||||||
identifier=instance.service_account_identifier, intent=TokenIntents.INTENT_API
|
|
||||||
).delete()
|
|
||||||
User.objects.filter(
|
User.objects.filter(
|
||||||
username=instance.service_account_identifier, type=UserTypes.INTERNAL_SERVICE_ACCOUNT
|
username=instance.service_account_identifier, type=UserTypes.INTERNAL_SERVICE_ACCOUNT
|
||||||
).delete()
|
).delete()
|
||||||
|
@ -8,6 +8,7 @@ from django.urls import reverse
|
|||||||
from rest_framework.exceptions import ValidationError
|
from rest_framework.exceptions import ValidationError
|
||||||
|
|
||||||
from authentik.brands.utils import get_brand_for_request
|
from authentik.brands.utils import get_brand_for_request
|
||||||
|
from authentik.core.middleware import RESPONSE_HEADER_ID
|
||||||
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
|
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
|
||||||
from authentik.events.models import Event, EventAction
|
from authentik.events.models import Event, EventAction
|
||||||
from authentik.flows.models import FlowDesignation, FlowStageBinding
|
from authentik.flows.models import FlowDesignation, FlowStageBinding
|
||||||
@ -186,6 +187,7 @@ class AuthenticatorValidateStageDuoTests(FlowTestCase):
|
|||||||
"method": "GET",
|
"method": "GET",
|
||||||
"path": f"/api/v3/flows/executor/{flow.slug}/",
|
"path": f"/api/v3/flows/executor/{flow.slug}/",
|
||||||
"user_agent": "",
|
"user_agent": "",
|
||||||
|
"request_id": response[RESPONSE_HEADER_ID],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -144,7 +144,7 @@ class Migration(migrations.Migration):
|
|||||||
default=None,
|
default=None,
|
||||||
help_text=(
|
help_text=(
|
||||||
"When set, shows a password field, instead of showing the password field as"
|
"When set, shows a password field, instead of showing the password field as"
|
||||||
" seaprate step."
|
" separate step."
|
||||||
),
|
),
|
||||||
null=True,
|
null=True,
|
||||||
on_delete=django.db.models.deletion.SET_NULL,
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
@ -108,7 +108,7 @@ class PromptViewSet(UsedByMixin, ModelViewSet):
|
|||||||
return Response(
|
return Response(
|
||||||
{
|
{
|
||||||
"non_field_errors": [
|
"non_field_errors": [
|
||||||
exception_to_string(exc),
|
exception_to_string(exc.exc),
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
status=400,
|
status=400,
|
||||||
|
@ -170,7 +170,7 @@ class Prompt(SerializerModel):
|
|||||||
try:
|
try:
|
||||||
raw_choices = evaluator.evaluate(self.placeholder)
|
raw_choices = evaluator.evaluate(self.placeholder)
|
||||||
except Exception as exc: # pylint:disable=broad-except
|
except Exception as exc: # pylint:disable=broad-except
|
||||||
wrapped = PropertyMappingExpressionException(str(exc))
|
wrapped = PropertyMappingExpressionException(exc, None)
|
||||||
LOGGER.warning(
|
LOGGER.warning(
|
||||||
"failed to evaluate prompt choices",
|
"failed to evaluate prompt choices",
|
||||||
exc=wrapped,
|
exc=wrapped,
|
||||||
@ -208,7 +208,7 @@ class Prompt(SerializerModel):
|
|||||||
try:
|
try:
|
||||||
return evaluator.evaluate(self.placeholder)
|
return evaluator.evaluate(self.placeholder)
|
||||||
except Exception as exc: # pylint:disable=broad-except
|
except Exception as exc: # pylint:disable=broad-except
|
||||||
wrapped = PropertyMappingExpressionException(str(exc), None)
|
wrapped = PropertyMappingExpressionException(exc, None)
|
||||||
LOGGER.warning(
|
LOGGER.warning(
|
||||||
"failed to evaluate prompt placeholder",
|
"failed to evaluate prompt placeholder",
|
||||||
exc=wrapped,
|
exc=wrapped,
|
||||||
@ -237,7 +237,7 @@ class Prompt(SerializerModel):
|
|||||||
try:
|
try:
|
||||||
value = evaluator.evaluate(self.initial_value)
|
value = evaluator.evaluate(self.initial_value)
|
||||||
except Exception as exc: # pylint:disable=broad-except
|
except Exception as exc: # pylint:disable=broad-except
|
||||||
wrapped = PropertyMappingExpressionException(str(exc))
|
wrapped = PropertyMappingExpressionException(exc, None)
|
||||||
LOGGER.warning(
|
LOGGER.warning(
|
||||||
"failed to evaluate prompt initial value",
|
"failed to evaluate prompt initial value",
|
||||||
exc=wrapped,
|
exc=wrapped,
|
||||||
|
@ -82,3 +82,5 @@ entries:
|
|||||||
order: 10
|
order: 10
|
||||||
target: !KeyOf default-authentication-flow-password-binding
|
target: !KeyOf default-authentication-flow-password-binding
|
||||||
policy: !KeyOf default-authentication-flow-password-optional
|
policy: !KeyOf default-authentication-flow-password-optional
|
||||||
|
attrs:
|
||||||
|
failure_result: true
|
||||||
|
@ -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.6.1 Blueprint schema",
|
"title": "authentik 2024.6.5 Blueprint schema",
|
||||||
"required": [
|
"required": [
|
||||||
"version",
|
"version",
|
||||||
"entries"
|
"entries"
|
||||||
|
@ -31,7 +31,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- redis:/data
|
- redis:/data
|
||||||
server:
|
server:
|
||||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.6.1}
|
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.6.5}
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command: server
|
command: server
|
||||||
environment:
|
environment:
|
||||||
@ -52,7 +52,7 @@ services:
|
|||||||
- postgresql
|
- postgresql
|
||||||
- redis
|
- redis
|
||||||
worker:
|
worker:
|
||||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.6.1}
|
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.6.5}
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command: worker
|
command: worker
|
||||||
environment:
|
environment:
|
||||||
|
2
go.mod
2
go.mod
@ -28,7 +28,7 @@ require (
|
|||||||
github.com/spf13/cobra v1.8.0
|
github.com/spf13/cobra v1.8.0
|
||||||
github.com/stretchr/testify v1.9.0
|
github.com/stretchr/testify v1.9.0
|
||||||
github.com/wwt/guac v1.3.2
|
github.com/wwt/guac v1.3.2
|
||||||
goauthentik.io/api/v3 v3.2024042.11
|
goauthentik.io/api/v3 v3.2024060.5
|
||||||
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
|
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
|
||||||
golang.org/x/oauth2 v0.21.0
|
golang.org/x/oauth2 v0.21.0
|
||||||
golang.org/x/sync v0.7.0
|
golang.org/x/sync v0.7.0
|
||||||
|
4
go.sum
4
go.sum
@ -294,8 +294,8 @@ go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y
|
|||||||
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
|
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
|
||||||
go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
|
go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
|
||||||
go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4=
|
go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4=
|
||||||
goauthentik.io/api/v3 v3.2024042.11 h1:cGgUz1E8rlMphGvv04VI7i+MgT8eidZbxTpza5zd96I=
|
goauthentik.io/api/v3 v3.2024060.5 h1:AjvPUZoObk7a86ZZaz2tmruteY+1vAEfVzIOzQpWSXM=
|
||||||
goauthentik.io/api/v3 v3.2024042.11/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
|
goauthentik.io/api/v3 v3.2024060.5/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
@ -29,4 +29,4 @@ func UserAgent() string {
|
|||||||
return fmt.Sprintf("authentik@%s", FullVersion())
|
return fmt.Sprintf("authentik@%s", FullVersion())
|
||||||
}
|
}
|
||||||
|
|
||||||
const VERSION = "2024.6.1"
|
const VERSION = "2024.6.5"
|
||||||
|
@ -183,7 +183,19 @@ func (ac *APIController) startWSHealth() {
|
|||||||
|
|
||||||
func (ac *APIController) startIntervalUpdater() {
|
func (ac *APIController) startIntervalUpdater() {
|
||||||
logger := ac.logger.WithField("loop", "interval-updater")
|
logger := ac.logger.WithField("loop", "interval-updater")
|
||||||
ticker := time.NewTicker(5 * time.Minute)
|
getInterval := func() time.Duration {
|
||||||
|
// Ensure timer interval is not negative or 0
|
||||||
|
// for 0 we assume migration or unconfigured, so default to 5 minutes
|
||||||
|
if ac.Outpost.RefreshIntervalS <= 0 {
|
||||||
|
return 5 * time.Minute
|
||||||
|
}
|
||||||
|
// Clamp interval to be at least 30 seconds
|
||||||
|
if ac.Outpost.RefreshIntervalS < 30 {
|
||||||
|
return 30 * time.Second
|
||||||
|
}
|
||||||
|
return time.Duration(ac.Outpost.RefreshIntervalS) * time.Second
|
||||||
|
}
|
||||||
|
ticker := time.NewTicker(getInterval())
|
||||||
for ; true; <-ticker.C {
|
for ; true; <-ticker.C {
|
||||||
logger.Debug("Running interval update")
|
logger.Debug("Running interval update")
|
||||||
err := ac.OnRefresh()
|
err := ac.OnRefresh()
|
||||||
@ -198,6 +210,7 @@ func (ac *APIController) startIntervalUpdater() {
|
|||||||
"build": constants.BUILD("tagged"),
|
"build": constants.BUILD("tagged"),
|
||||||
}).SetToCurrentTime()
|
}).SetToCurrentTime()
|
||||||
}
|
}
|
||||||
|
ticker.Reset(getInterval())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
# flake8: noqa
|
# flake8: noqa
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
from authentik.lib.config import CONFIG
|
||||||
from lifecycle.migrate import BaseMigration
|
from lifecycle.migrate import BaseMigration
|
||||||
|
|
||||||
MEDIA_ROOT = Path(__file__).parent.parent.parent / "media"
|
MEDIA_ROOT = Path(__file__).parent.parent.parent / "media"
|
||||||
@ -9,7 +10,9 @@ TENANT_MEDIA_ROOT = MEDIA_ROOT / "public"
|
|||||||
|
|
||||||
class Migration(BaseMigration):
|
class Migration(BaseMigration):
|
||||||
def needs_migration(self) -> bool:
|
def needs_migration(self) -> bool:
|
||||||
return not TENANT_MEDIA_ROOT.exists()
|
return (
|
||||||
|
not TENANT_MEDIA_ROOT.exists() and CONFIG.get("storage.media.backend", "file") != "s3"
|
||||||
|
)
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
TENANT_MEDIA_ROOT.mkdir(parents=True)
|
TENANT_MEDIA_ROOT.mkdir(parents=True)
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "@goauthentik/authentik",
|
"name": "@goauthentik/authentik",
|
||||||
"version": "2024.6.1",
|
"version": "2024.6.5",
|
||||||
"private": true
|
"private": true
|
||||||
}
|
}
|
||||||
|
29
poetry.lock
generated
29
poetry.lock
generated
@ -1,4 +1,4 @@
|
|||||||
# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand.
|
# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aiohttp"
|
name = "aiohttp"
|
||||||
@ -1513,20 +1513,6 @@ files = [
|
|||||||
dnspython = ">=2.0.0"
|
dnspython = ">=2.0.0"
|
||||||
idna = ">=2.0.0"
|
idna = ">=2.0.0"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "facebook-sdk"
|
|
||||||
version = "3.1.0"
|
|
||||||
description = "This client library is designed to support the Facebook Graph API and the official Facebook JavaScript SDK, which is the canonical way to implement Facebook authentication."
|
|
||||||
optional = false
|
|
||||||
python-versions = "*"
|
|
||||||
files = [
|
|
||||||
{file = "facebook-sdk-3.1.0.tar.gz", hash = "sha256:cabcd2e69ea3d9f042919c99b353df7aa1e2be86d040121f6e9f5e63c1cf0f8d"},
|
|
||||||
{file = "facebook_sdk-3.1.0-py2.py3-none-any.whl", hash = "sha256:2e987b3e0f466a6f4ee77b935eb023dba1384134f004a2af21f1cfff7fe0806e"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.dependencies]
|
|
||||||
requests = "*"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fido2"
|
name = "fido2"
|
||||||
version = "1.1.3"
|
version = "1.1.3"
|
||||||
@ -2954,9 +2940,14 @@ version = "0.0.14"
|
|||||||
description = "Python module for oci specifications"
|
description = "Python module for oci specifications"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "*"
|
python-versions = "*"
|
||||||
files = [
|
files = []
|
||||||
{file = "opencontainers-0.0.14.tar.gz", hash = "sha256:fde3b8099b56b5c956415df8933e2227e1914e805a277b844f2f9e52341738f2"},
|
develop = false
|
||||||
]
|
|
||||||
|
[package.source]
|
||||||
|
type = "git"
|
||||||
|
url = "https://github.com/vsoch/oci-python"
|
||||||
|
reference = "20d69d9cc50a0fef31605b46f06da0c94f1ec3cf"
|
||||||
|
resolved_reference = "20d69d9cc50a0fef31605b46f06da0c94f1ec3cf"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "opentelemetry-api"
|
name = "opentelemetry-api"
|
||||||
@ -5350,4 +5341,4 @@ files = [
|
|||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.0"
|
||||||
python-versions = "~3.12"
|
python-versions = "~3.12"
|
||||||
content-hash = "f960013b56683ab42d82f8b49b2822dffc76046e3d22695ebb737b405a98dbaf"
|
content-hash = "055376879ff784080ab95c02eaa012fb1dad1213b1faa0dd1d61b0b812859b6d"
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "authentik"
|
name = "authentik"
|
||||||
version = "2024.6.1"
|
version = "2024.6.5"
|
||||||
description = ""
|
description = ""
|
||||||
authors = ["authentik Team <hello@goauthentik.io>"]
|
authors = ["authentik Team <hello@goauthentik.io>"]
|
||||||
|
|
||||||
@ -110,7 +110,6 @@ docker = "*"
|
|||||||
drf-spectacular = "*"
|
drf-spectacular = "*"
|
||||||
dumb-init = "*"
|
dumb-init = "*"
|
||||||
duo-client = "*"
|
duo-client = "*"
|
||||||
facebook-sdk = "*"
|
|
||||||
fido2 = "*"
|
fido2 = "*"
|
||||||
flower = "*"
|
flower = "*"
|
||||||
geoip2 = "*"
|
geoip2 = "*"
|
||||||
@ -121,7 +120,7 @@ kubernetes = "*"
|
|||||||
ldap3 = "*"
|
ldap3 = "*"
|
||||||
lxml = "*"
|
lxml = "*"
|
||||||
msgraph-sdk = "*"
|
msgraph-sdk = "*"
|
||||||
opencontainers = { extras = ["reggie"], version = "*" }
|
opencontainers = { git = "https://github.com/vsoch/oci-python", rev = "20d69d9cc50a0fef31605b46f06da0c94f1ec3cf", extras = ["reggie"] }
|
||||||
packaging = "*"
|
packaging = "*"
|
||||||
paramiko = "*"
|
paramiko = "*"
|
||||||
psycopg = { extras = ["c"], version = "*" }
|
psycopg = { extras = ["c"], version = "*" }
|
||||||
|
15
schema.yml
15
schema.yml
@ -1,7 +1,7 @@
|
|||||||
openapi: 3.0.3
|
openapi: 3.0.3
|
||||||
info:
|
info:
|
||||||
title: authentik
|
title: authentik
|
||||||
version: 2024.6.1
|
version: 2024.6.5
|
||||||
description: Making authentication simple.
|
description: Making authentication simple.
|
||||||
contact:
|
contact:
|
||||||
email: hello@goauthentik.io
|
email: hello@goauthentik.io
|
||||||
@ -13080,6 +13080,15 @@ paths:
|
|||||||
name: identifier
|
name: identifier
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
|
- in: query
|
||||||
|
name: identifier_in
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
description: Multiple values may be separated by commas.
|
||||||
|
explode: false
|
||||||
|
style: form
|
||||||
- in: query
|
- in: query
|
||||||
name: ip
|
name: ip
|
||||||
schema:
|
schema:
|
||||||
@ -39489,6 +39498,9 @@ components:
|
|||||||
allOf:
|
allOf:
|
||||||
- $ref: '#/components/schemas/ServiceConnection'
|
- $ref: '#/components/schemas/ServiceConnection'
|
||||||
readOnly: true
|
readOnly: true
|
||||||
|
refresh_interval_s:
|
||||||
|
type: integer
|
||||||
|
readOnly: true
|
||||||
token_identifier:
|
token_identifier:
|
||||||
type: string
|
type: string
|
||||||
description: Get Token identifier
|
description: Get Token identifier
|
||||||
@ -39510,6 +39522,7 @@ components:
|
|||||||
- pk
|
- pk
|
||||||
- providers
|
- providers
|
||||||
- providers_obj
|
- providers_obj
|
||||||
|
- refresh_interval_s
|
||||||
- service_connection_obj
|
- service_connection_obj
|
||||||
- token_identifier
|
- token_identifier
|
||||||
- type
|
- type
|
||||||
|
@ -5,7 +5,6 @@ from time import sleep
|
|||||||
|
|
||||||
from docker.client import DockerClient, from_env
|
from docker.client import DockerClient, from_env
|
||||||
from docker.models.containers import Container
|
from docker.models.containers import Container
|
||||||
from guardian.shortcuts import get_anonymous_user
|
|
||||||
from ldap3 import ALL, ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES, SUBTREE, Connection, Server
|
from ldap3 import ALL, ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES, SUBTREE, Connection, Server
|
||||||
from ldap3.core.exceptions import LDAPInvalidCredentialsResult
|
from ldap3.core.exceptions import LDAPInvalidCredentialsResult
|
||||||
|
|
||||||
@ -180,15 +179,13 @@ class TestProviderLDAP(SeleniumTestCase):
|
|||||||
)
|
)
|
||||||
with self.assertRaises(LDAPInvalidCredentialsResult):
|
with self.assertRaises(LDAPInvalidCredentialsResult):
|
||||||
_connection.bind()
|
_connection.bind()
|
||||||
anon = get_anonymous_user()
|
|
||||||
self.assertTrue(
|
self.assertTrue(
|
||||||
Event.objects.filter(
|
Event.objects.filter(
|
||||||
action=EventAction.LOGIN_FAILED,
|
action=EventAction.LOGIN_FAILED,
|
||||||
user={
|
user={
|
||||||
"pk": anon.pk,
|
"pk": self.user.pk,
|
||||||
"email": anon.email,
|
"email": self.user.email,
|
||||||
"username": anon.username,
|
"username": self.user.username,
|
||||||
"is_anonymous": True,
|
|
||||||
},
|
},
|
||||||
).exists(),
|
).exists(),
|
||||||
)
|
)
|
||||||
|
2
web/package-lock.json
generated
2
web/package-lock.json
generated
@ -17,7 +17,7 @@
|
|||||||
"@codemirror/theme-one-dark": "^6.1.2",
|
"@codemirror/theme-one-dark": "^6.1.2",
|
||||||
"@formatjs/intl-listformat": "^7.5.7",
|
"@formatjs/intl-listformat": "^7.5.7",
|
||||||
"@fortawesome/fontawesome-free": "^6.5.2",
|
"@fortawesome/fontawesome-free": "^6.5.2",
|
||||||
"@goauthentik/api": "^2024.6.0-1720200294",
|
"@goauthentik/api": "^2024.6.0-1719577139",
|
||||||
"@lit/context": "^1.1.2",
|
"@lit/context": "^1.1.2",
|
||||||
"@lit/localize": "^0.12.1",
|
"@lit/localize": "^0.12.1",
|
||||||
"@lit/reactive-element": "^2.0.4",
|
"@lit/reactive-element": "^2.0.4",
|
||||||
|
@ -38,7 +38,7 @@
|
|||||||
"@codemirror/theme-one-dark": "^6.1.2",
|
"@codemirror/theme-one-dark": "^6.1.2",
|
||||||
"@formatjs/intl-listformat": "^7.5.7",
|
"@formatjs/intl-listformat": "^7.5.7",
|
||||||
"@fortawesome/fontawesome-free": "^6.5.2",
|
"@fortawesome/fontawesome-free": "^6.5.2",
|
||||||
"@goauthentik/api": "^2024.6.0-1720200294",
|
"@goauthentik/api": "^2024.6.0-1719577139",
|
||||||
"@lit/context": "^1.1.2",
|
"@lit/context": "^1.1.2",
|
||||||
"@lit/localize": "^0.12.1",
|
"@lit/localize": "^0.12.1",
|
||||||
"@lit/reactive-element": "^2.0.4",
|
"@lit/reactive-element": "^2.0.4",
|
||||||
|
@ -56,6 +56,7 @@ export class OutpostStatusChart extends AKChart<SummarizedSyncStatus[]> {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
this.centerText = outposts.pagination.count.toString();
|
this.centerText = outposts.pagination.count.toString();
|
||||||
|
outpostStats.sort((a, b) => a.label.localeCompare(b.label));
|
||||||
return outpostStats;
|
return outpostStats;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,3 +1,7 @@
|
|||||||
|
import {
|
||||||
|
digestAlgorithmOptions,
|
||||||
|
signatureAlgorithmOptions,
|
||||||
|
} from "@goauthentik/admin/applications/wizard/methods/saml/SamlProviderOptions";
|
||||||
import "@goauthentik/admin/common/ak-crypto-certificate-search";
|
import "@goauthentik/admin/common/ak-crypto-certificate-search";
|
||||||
import "@goauthentik/admin/common/ak-flow-search/ak-flow-search";
|
import "@goauthentik/admin/common/ak-flow-search/ak-flow-search";
|
||||||
import { BaseProviderForm } from "@goauthentik/admin/providers/BaseProviderForm";
|
import { BaseProviderForm } from "@goauthentik/admin/providers/BaseProviderForm";
|
||||||
@ -14,7 +18,6 @@ import { customElement } from "lit/decorators.js";
|
|||||||
import { ifDefined } from "lit/directives/if-defined.js";
|
import { ifDefined } from "lit/directives/if-defined.js";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DigestAlgorithmEnum,
|
|
||||||
FlowsInstancesListDesignationEnum,
|
FlowsInstancesListDesignationEnum,
|
||||||
PaginatedSAMLPropertyMappingList,
|
PaginatedSAMLPropertyMappingList,
|
||||||
PropertymappingsApi,
|
PropertymappingsApi,
|
||||||
@ -22,7 +25,6 @@ import {
|
|||||||
ProvidersApi,
|
ProvidersApi,
|
||||||
SAMLPropertyMapping,
|
SAMLPropertyMapping,
|
||||||
SAMLProvider,
|
SAMLProvider,
|
||||||
SignatureAlgorithmEnum,
|
|
||||||
SpBindingEnum,
|
SpBindingEnum,
|
||||||
} from "@goauthentik/api";
|
} from "@goauthentik/api";
|
||||||
|
|
||||||
@ -333,25 +335,7 @@ export class SAMLProviderFormPage extends BaseProviderForm<SAMLProvider> {
|
|||||||
name="digestAlgorithm"
|
name="digestAlgorithm"
|
||||||
>
|
>
|
||||||
<ak-radio
|
<ak-radio
|
||||||
.options=${[
|
.options=${digestAlgorithmOptions}
|
||||||
{
|
|
||||||
label: "SHA1",
|
|
||||||
value: DigestAlgorithmEnum._200009Xmldsigsha1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "SHA256",
|
|
||||||
value: DigestAlgorithmEnum._200104Xmlencsha256,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "SHA384",
|
|
||||||
value: DigestAlgorithmEnum._200104XmldsigMoresha384,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "SHA512",
|
|
||||||
value: DigestAlgorithmEnum._200104Xmlencsha512,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
.value=${this.instance?.digestAlgorithm}
|
.value=${this.instance?.digestAlgorithm}
|
||||||
>
|
>
|
||||||
</ak-radio>
|
</ak-radio>
|
||||||
@ -362,29 +346,7 @@ export class SAMLProviderFormPage extends BaseProviderForm<SAMLProvider> {
|
|||||||
name="signatureAlgorithm"
|
name="signatureAlgorithm"
|
||||||
>
|
>
|
||||||
<ak-radio
|
<ak-radio
|
||||||
.options=${[
|
.options=${signatureAlgorithmOptions}
|
||||||
{
|
|
||||||
label: "RSA-SHA1",
|
|
||||||
value: SignatureAlgorithmEnum._200009XmldsigrsaSha1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "RSA-SHA256",
|
|
||||||
value: SignatureAlgorithmEnum._200104XmldsigMorersaSha256,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "RSA-SHA384",
|
|
||||||
value: SignatureAlgorithmEnum._200104XmldsigMorersaSha384,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "RSA-SHA512",
|
|
||||||
value: SignatureAlgorithmEnum._200104XmldsigMorersaSha512,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "DSA-SHA1",
|
|
||||||
value: SignatureAlgorithmEnum._200009XmldsigdsaSha1,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
.value=${this.instance?.signatureAlgorithm}
|
.value=${this.instance?.signatureAlgorithm}
|
||||||
>
|
>
|
||||||
</ak-radio>
|
</ak-radio>
|
||||||
|
@ -36,11 +36,13 @@ import "@goauthentik/elements/oauth/UserRefreshTokenList";
|
|||||||
import "@goauthentik/elements/rbac/ObjectPermissionsPage";
|
import "@goauthentik/elements/rbac/ObjectPermissionsPage";
|
||||||
import "@goauthentik/elements/user/SessionList";
|
import "@goauthentik/elements/user/SessionList";
|
||||||
import "@goauthentik/elements/user/UserConsentList";
|
import "@goauthentik/elements/user/UserConsentList";
|
||||||
|
import "@goauthentik/elements/user/UserReputationList";
|
||||||
import "@goauthentik/elements/user/sources/SourceSettings";
|
import "@goauthentik/elements/user/sources/SourceSettings";
|
||||||
|
|
||||||
import { msg, str } from "@lit/localize";
|
import { msg, str } from "@lit/localize";
|
||||||
import { TemplateResult, css, html, nothing } from "lit";
|
import { TemplateResult, css, 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 PFBanner from "@patternfly/patternfly/components/Banner/banner.css";
|
import PFBanner from "@patternfly/patternfly/components/Banner/banner.css";
|
||||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||||
@ -274,6 +276,21 @@ export class UserViewPage extends WithCapabilitiesConfig(AKElement) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
<section
|
||||||
|
slot="page-reputation"
|
||||||
|
data-tab-title="${msg("Reputation scores")}"
|
||||||
|
class="pf-c-page__main-section pf-m-no-padding-mobile"
|
||||||
|
>
|
||||||
|
<div class="pf-c-card">
|
||||||
|
<div class="pf-c-card__body">
|
||||||
|
<ak-user-reputation-list
|
||||||
|
targetUsername=${user.username}
|
||||||
|
targetEmail=${ifDefined(user.email)}
|
||||||
|
>
|
||||||
|
</ak-user-reputation-list>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
<section
|
<section
|
||||||
slot="page-consent"
|
slot="page-consent"
|
||||||
data-tab-title="${msg("Explicit Consent")}"
|
data-tab-title="${msg("Explicit Consent")}"
|
||||||
|
@ -3,7 +3,7 @@ export const SUCCESS_CLASS = "pf-m-success";
|
|||||||
export const ERROR_CLASS = "pf-m-danger";
|
export const ERROR_CLASS = "pf-m-danger";
|
||||||
export const PROGRESS_CLASS = "pf-m-in-progress";
|
export const PROGRESS_CLASS = "pf-m-in-progress";
|
||||||
export const CURRENT_CLASS = "pf-m-current";
|
export const CURRENT_CLASS = "pf-m-current";
|
||||||
export const VERSION = "2024.6.1";
|
export const VERSION = "2024.6.5";
|
||||||
export const TITLE_DEFAULT = "authentik";
|
export const TITLE_DEFAULT = "authentik";
|
||||||
export const ROUTE_SEPARATOR = ";";
|
export const ROUTE_SEPARATOR = ";";
|
||||||
|
|
||||||
|
@ -42,6 +42,14 @@ body {
|
|||||||
--pf-c-card--BackgroundColor: var(--ak-dark-background-light);
|
--pf-c-card--BackgroundColor: var(--ak-dark-background-light);
|
||||||
color: var(--ak-dark-foreground);
|
color: var(--ak-dark-foreground);
|
||||||
}
|
}
|
||||||
|
.pf-c-card.pf-m-non-selectable-raised {
|
||||||
|
--pf-c-card--BackgroundColor: var(--ak-dark-background-lighter);
|
||||||
|
}
|
||||||
|
.pf-c-card.pf-m-hoverable-raised::before,
|
||||||
|
.pf-c-card.pf-m-selectable-raised::before,
|
||||||
|
.pf-c-card.pf-m-non-selectable-raised::before {
|
||||||
|
--pf-c-card--m-selectable-raised--before--BackgroundColor: var(--ak-dark-background-light);
|
||||||
|
}
|
||||||
.pf-c-card__title,
|
.pf-c-card__title,
|
||||||
.pf-c-card__body {
|
.pf-c-card__body {
|
||||||
color: var(--ak-dark-foreground);
|
color: var(--ak-dark-foreground);
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { EVENT_THEME_CHANGE } from "@goauthentik/common/constants";
|
import { EVENT_THEME_CHANGE } from "@goauthentik/common/constants";
|
||||||
|
import { globalAK } from "@goauthentik/common/global";
|
||||||
import { UIConfig } from "@goauthentik/common/ui/config";
|
import { UIConfig } from "@goauthentik/common/ui/config";
|
||||||
import { adaptCSS } from "@goauthentik/common/utils";
|
import { adaptCSS } from "@goauthentik/common/utils";
|
||||||
import { ensureCSSStyleSheet } from "@goauthentik/elements/utils/ensureCSSStyleSheet";
|
import { ensureCSSStyleSheet } from "@goauthentik/elements/utils/ensureCSSStyleSheet";
|
||||||
@ -16,6 +17,7 @@ type AkInterface = HTMLElement & {
|
|||||||
brand?: CurrentBrand;
|
brand?: CurrentBrand;
|
||||||
uiConfig?: UIConfig;
|
uiConfig?: UIConfig;
|
||||||
config?: Config;
|
config?: Config;
|
||||||
|
get activeTheme(): UiThemeEnum | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const rootInterface = <T extends AkInterface>(): T | undefined =>
|
export const rootInterface = <T extends AkInterface>(): T | undefined =>
|
||||||
@ -41,7 +43,11 @@ function fetchCustomCSS(): Promise<string[]> {
|
|||||||
return css;
|
return css;
|
||||||
}
|
}
|
||||||
|
|
||||||
const QUERY_MEDIA_COLOR_LIGHT = "(prefers-color-scheme: light)";
|
export const QUERY_MEDIA_COLOR_LIGHT = "(prefers-color-scheme: light)";
|
||||||
|
|
||||||
|
// Ensure themes are converted to a static instance of CSS Stylesheet, otherwise the
|
||||||
|
// when changing themes we might not remove the correct css stylesheet instance.
|
||||||
|
const _darkTheme = ensureCSSStyleSheet(ThemeDark);
|
||||||
|
|
||||||
@localized()
|
@localized()
|
||||||
export class AKElement extends LitElement {
|
export class AKElement extends LitElement {
|
||||||
@ -90,12 +96,7 @@ export class AKElement extends LitElement {
|
|||||||
async _initTheme(root: DocumentOrShadowRoot): Promise<void> {
|
async _initTheme(root: DocumentOrShadowRoot): Promise<void> {
|
||||||
// Early activate theme based on media query to prevent light flash
|
// Early activate theme based on media query to prevent light flash
|
||||||
// when dark is preferred
|
// when dark is preferred
|
||||||
this._activateTheme(
|
this._applyTheme(root, globalAK().brand.uiTheme);
|
||||||
root,
|
|
||||||
window.matchMedia(QUERY_MEDIA_COLOR_LIGHT).matches
|
|
||||||
? UiThemeEnum.Light
|
|
||||||
: UiThemeEnum.Dark,
|
|
||||||
);
|
|
||||||
this._applyTheme(root, await this.getTheme());
|
this._applyTheme(root, await this.getTheme());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -125,8 +126,9 @@ export class AKElement extends LitElement {
|
|||||||
ev?.matches || this._mediaMatcher?.matches
|
ev?.matches || this._mediaMatcher?.matches
|
||||||
? UiThemeEnum.Light
|
? UiThemeEnum.Light
|
||||||
: UiThemeEnum.Dark;
|
: UiThemeEnum.Dark;
|
||||||
this._activateTheme(root, theme);
|
this._activateTheme(theme, root);
|
||||||
};
|
};
|
||||||
|
this._mediaMatcherHandler(undefined);
|
||||||
this._mediaMatcher.addEventListener("change", this._mediaMatcherHandler);
|
this._mediaMatcher.addEventListener("change", this._mediaMatcherHandler);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@ -136,17 +138,21 @@ export class AKElement extends LitElement {
|
|||||||
this._mediaMatcher.removeEventListener("change", this._mediaMatcherHandler);
|
this._mediaMatcher.removeEventListener("change", this._mediaMatcherHandler);
|
||||||
this._mediaMatcher = undefined;
|
this._mediaMatcher = undefined;
|
||||||
}
|
}
|
||||||
this._activateTheme(root, theme);
|
this._activateTheme(theme, root);
|
||||||
}
|
}
|
||||||
|
|
||||||
static themeToStylesheet(theme?: UiThemeEnum): CSSStyleSheet | undefined {
|
static themeToStylesheet(theme?: UiThemeEnum): CSSStyleSheet | undefined {
|
||||||
if (theme === UiThemeEnum.Dark) {
|
if (theme === UiThemeEnum.Dark) {
|
||||||
return ThemeDark;
|
return _darkTheme;
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
_activateTheme(root: DocumentOrShadowRoot, theme: UiThemeEnum) {
|
/**
|
||||||
|
* Directly activate a given theme, accepts multiple document/ShadowDOMs to apply the stylesheet
|
||||||
|
* to. The stylesheets are applied to each DOM in order. Does nothing if the given theme is already active.
|
||||||
|
*/
|
||||||
|
_activateTheme(theme: UiThemeEnum, ...roots: DocumentOrShadowRoot[]) {
|
||||||
if (theme === this._activeTheme) {
|
if (theme === this._activeTheme) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -161,12 +167,19 @@ export class AKElement extends LitElement {
|
|||||||
this.setAttribute("theme", theme);
|
this.setAttribute("theme", theme);
|
||||||
const stylesheet = AKElement.themeToStylesheet(theme);
|
const stylesheet = AKElement.themeToStylesheet(theme);
|
||||||
const oldStylesheet = AKElement.themeToStylesheet(this._activeTheme);
|
const oldStylesheet = AKElement.themeToStylesheet(this._activeTheme);
|
||||||
if (stylesheet) {
|
roots.forEach((root) => {
|
||||||
root.adoptedStyleSheets = [...root.adoptedStyleSheets, ensureCSSStyleSheet(stylesheet)];
|
if (stylesheet) {
|
||||||
}
|
root.adoptedStyleSheets = [
|
||||||
if (oldStylesheet) {
|
...root.adoptedStyleSheets,
|
||||||
root.adoptedStyleSheets = root.adoptedStyleSheets.filter((v) => v !== oldStylesheet);
|
ensureCSSStyleSheet(stylesheet),
|
||||||
}
|
];
|
||||||
|
}
|
||||||
|
if (oldStylesheet) {
|
||||||
|
root.adoptedStyleSheets = root.adoptedStyleSheets.filter(
|
||||||
|
(v) => v !== oldStylesheet,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
this._activeTheme = theme;
|
this._activeTheme = theme;
|
||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@ import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
|||||||
import type { Config, CurrentBrand, LicenseSummary } from "@goauthentik/api";
|
import type { Config, CurrentBrand, LicenseSummary } from "@goauthentik/api";
|
||||||
import { UiThemeEnum } from "@goauthentik/api";
|
import { UiThemeEnum } from "@goauthentik/api";
|
||||||
|
|
||||||
import { AKElement } from "../Base";
|
import { AKElement, rootInterface } from "../Base";
|
||||||
import { BrandContextController } from "./BrandContextController";
|
import { BrandContextController } from "./BrandContextController";
|
||||||
import { ConfigContextController } from "./ConfigContextController";
|
import { ConfigContextController } from "./ConfigContextController";
|
||||||
import { EnterpriseContextController } from "./EnterpriseContextController";
|
import { EnterpriseContextController } from "./EnterpriseContextController";
|
||||||
@ -50,9 +50,19 @@ export class Interface extends AKElement implements AkInterface {
|
|||||||
this.dataset.akInterfaceRoot = "true";
|
this.dataset.akInterfaceRoot = "true";
|
||||||
}
|
}
|
||||||
|
|
||||||
_activateTheme(root: DocumentOrShadowRoot, theme: UiThemeEnum): void {
|
_activateTheme(theme: UiThemeEnum, ...roots: DocumentOrShadowRoot[]): void {
|
||||||
super._activateTheme(root, theme);
|
if (theme === this._activeTheme) {
|
||||||
super._activateTheme(document as unknown as DocumentOrShadowRoot, theme);
|
return;
|
||||||
|
}
|
||||||
|
console.debug(
|
||||||
|
`authentik/interface[${rootInterface()?.tagName.toLowerCase()}]: Enabling theme ${theme}`,
|
||||||
|
);
|
||||||
|
// Special case for root interfaces, as they need to modify the global document CSS too
|
||||||
|
// Instead of calling ._activateTheme() twice, we insert the root document in the call
|
||||||
|
// since multiple calls to ._activateTheme() would not do anything after the first call
|
||||||
|
// as the theme is already enabled.
|
||||||
|
roots.unshift(document as unknown as DocumentOrShadowRoot);
|
||||||
|
super._activateTheme(theme, ...roots);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getTheme(): Promise<UiThemeEnum> {
|
async getTheme(): Promise<UiThemeEnum> {
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import { EVENT_LOCALE_CHANGE, EVENT_LOCALE_REQUEST } from "@goauthentik/common/constants";
|
import { EVENT_LOCALE_CHANGE, EVENT_LOCALE_REQUEST } from "@goauthentik/common/constants";
|
||||||
|
import { AKElement } from "@goauthentik/elements/Base";
|
||||||
import { customEvent } from "@goauthentik/elements/utils/customEvents";
|
import { customEvent } from "@goauthentik/elements/utils/customEvents";
|
||||||
|
|
||||||
import { LitElement, html } from "lit";
|
import { html } from "lit";
|
||||||
import { customElement, property } from "lit/decorators.js";
|
import { customElement, property } from "lit/decorators.js";
|
||||||
|
|
||||||
import { WithBrandConfig } from "../Interface/brandProvider";
|
import { WithBrandConfig } from "../Interface/brandProvider";
|
||||||
@ -9,8 +10,6 @@ import { initializeLocalization } from "./configureLocale";
|
|||||||
import type { LocaleGetter, LocaleSetter } from "./configureLocale";
|
import type { LocaleGetter, LocaleSetter } from "./configureLocale";
|
||||||
import { DEFAULT_LOCALE, autoDetectLanguage, getBestMatchLocale } from "./helpers";
|
import { DEFAULT_LOCALE, autoDetectLanguage, getBestMatchLocale } from "./helpers";
|
||||||
|
|
||||||
const LocaleContextBase = WithBrandConfig(LitElement);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A component to manage your locale settings.
|
* A component to manage your locale settings.
|
||||||
*
|
*
|
||||||
@ -25,7 +24,7 @@ const LocaleContextBase = WithBrandConfig(LitElement);
|
|||||||
* @fires ak-locale-change - When a valid locale has been swapped in
|
* @fires ak-locale-change - When a valid locale has been swapped in
|
||||||
*/
|
*/
|
||||||
@customElement("ak-locale-context")
|
@customElement("ak-locale-context")
|
||||||
export class LocaleContext extends LocaleContextBase {
|
export class LocaleContext extends WithBrandConfig(AKElement) {
|
||||||
/// @attribute The text representation of the current locale */
|
/// @attribute The text representation of the current locale */
|
||||||
@property({ attribute: true, type: String })
|
@property({ attribute: true, type: String })
|
||||||
locale = DEFAULT_LOCALE;
|
locale = DEFAULT_LOCALE;
|
||||||
@ -78,7 +77,7 @@ export class LocaleContext extends LocaleContextBase {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
locale.locale().then(() => {
|
locale.locale().then(() => {
|
||||||
console.debug(`Setting Locale to ... ${locale.label()} (${locale.code})`);
|
console.debug(`authentik/locale: Setting Locale to ${locale.label()} (${locale.code})`);
|
||||||
this.setLocale(locale.code).then(() => {
|
this.setLocale(locale.code).then(() => {
|
||||||
window.setTimeout(this.notifyApplication, 0);
|
window.setTimeout(this.notifyApplication, 0);
|
||||||
});
|
});
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { EVENT_SIDEBAR_TOGGLE } from "@goauthentik/common/constants";
|
import { EVENT_SIDEBAR_TOGGLE } from "@goauthentik/common/constants";
|
||||||
import { AKElement } from "@goauthentik/elements/Base";
|
import { AKElement } from "@goauthentik/elements/Base";
|
||||||
import { WithBrandConfig } from "@goauthentik/elements/Interface/brandProvider";
|
import { WithBrandConfig } from "@goauthentik/elements/Interface/brandProvider";
|
||||||
|
import { themeImage } from "@goauthentik/elements/utils/images";
|
||||||
|
|
||||||
import { CSSResult, TemplateResult, css, html } from "lit";
|
import { CSSResult, TemplateResult, css, html } from "lit";
|
||||||
import { customElement } from "lit/decorators.js";
|
import { customElement } from "lit/decorators.js";
|
||||||
@ -84,7 +85,7 @@ export class SidebarBrand extends WithBrandConfig(AKElement) {
|
|||||||
<a href="#/" class="pf-c-page__header-brand-link">
|
<a href="#/" class="pf-c-page__header-brand-link">
|
||||||
<div class="pf-c-brand ak-brand">
|
<div class="pf-c-brand ak-brand">
|
||||||
<img
|
<img
|
||||||
src=${this.brand?.brandingLogo ?? DefaultBrand.brandingLogo}
|
src=${themeImage(this.brand?.brandingLogo ?? DefaultBrand.brandingLogo)}
|
||||||
alt="authentik Logo"
|
alt="authentik Logo"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
|
83
web/src/elements/user/UserReputationList.ts
Normal file
83
web/src/elements/user/UserReputationList.ts
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||||
|
import { uiConfig } from "@goauthentik/common/ui/config";
|
||||||
|
import { getRelativeTime } from "@goauthentik/common/utils";
|
||||||
|
import "@goauthentik/elements/forms/DeleteBulkForm";
|
||||||
|
import { PaginatedResponse } from "@goauthentik/elements/table/Table";
|
||||||
|
import { Table, TableColumn } from "@goauthentik/elements/table/Table";
|
||||||
|
import getUnicodeFlagIcon from "country-flag-icons/unicode";
|
||||||
|
|
||||||
|
import { msg } from "@lit/localize";
|
||||||
|
import { TemplateResult, html } from "lit";
|
||||||
|
import { customElement, property } from "lit/decorators.js";
|
||||||
|
|
||||||
|
import { PoliciesApi, Reputation } from "@goauthentik/api";
|
||||||
|
|
||||||
|
@customElement("ak-user-reputation-list")
|
||||||
|
export class UserReputationList extends Table<Reputation> {
|
||||||
|
@property()
|
||||||
|
targetUsername!: string;
|
||||||
|
|
||||||
|
@property()
|
||||||
|
targetEmail!: string | undefined;
|
||||||
|
|
||||||
|
async apiEndpoint(page: number): Promise<PaginatedResponse<Reputation>> {
|
||||||
|
const identifiers = [this.targetUsername];
|
||||||
|
if (this.targetEmail !== undefined) {
|
||||||
|
identifiers.push(this.targetEmail);
|
||||||
|
}
|
||||||
|
return new PoliciesApi(DEFAULT_CONFIG).policiesReputationScoresList({
|
||||||
|
identifierIn: identifiers,
|
||||||
|
ordering: this.order,
|
||||||
|
page: page,
|
||||||
|
pageSize: (await uiConfig()).pagination.perPage,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
checkbox = true;
|
||||||
|
clearOnRefresh = true;
|
||||||
|
order = "identifier";
|
||||||
|
|
||||||
|
columns(): TableColumn[] {
|
||||||
|
return [
|
||||||
|
new TableColumn(msg("Identifier"), "identifier"),
|
||||||
|
new TableColumn(msg("IP"), "ip"),
|
||||||
|
new TableColumn(msg("Score"), "score"),
|
||||||
|
new TableColumn(msg("Updated"), "updated"),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
renderToolbarSelected(): TemplateResult {
|
||||||
|
const disabled = this.selectedElements.length < 1;
|
||||||
|
return html`<ak-forms-delete-bulk
|
||||||
|
objectLabel=${msg("Reputation score(s)")}
|
||||||
|
.objects=${this.selectedElements}
|
||||||
|
.usedBy=${(item: Reputation) => {
|
||||||
|
return new PoliciesApi(DEFAULT_CONFIG).policiesReputationScoresUsedByList({
|
||||||
|
reputationUuid: item.pk || "",
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
.delete=${(item: Reputation) => {
|
||||||
|
return new PoliciesApi(DEFAULT_CONFIG).policiesReputationScoresDestroy({
|
||||||
|
reputationUuid: item.pk || "",
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<button ?disabled=${disabled} slot="trigger" class="pf-c-button pf-m-danger">
|
||||||
|
${msg("Delete")}
|
||||||
|
</button>
|
||||||
|
</ak-forms-delete-bulk>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
row(item: Reputation): TemplateResult[] {
|
||||||
|
return [
|
||||||
|
html`${item.identifier}`,
|
||||||
|
html`${item.ipGeoData?.country
|
||||||
|
? html` ${getUnicodeFlagIcon(item.ipGeoData.country)} `
|
||||||
|
: html``}
|
||||||
|
${item.ip}`,
|
||||||
|
html`${item.score}`,
|
||||||
|
html`<div>${getRelativeTime(item.updated)}</div>
|
||||||
|
<small>${item.updated.toLocaleString()}</small>`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
13
web/src/elements/utils/images.ts
Normal file
13
web/src/elements/utils/images.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { QUERY_MEDIA_COLOR_LIGHT, rootInterface } from "@goauthentik/elements/Base";
|
||||||
|
|
||||||
|
import { UiThemeEnum } from "@goauthentik/api";
|
||||||
|
|
||||||
|
export function themeImage(rawPath: string) {
|
||||||
|
let enabledTheme = rootInterface()?.activeTheme;
|
||||||
|
if (!enabledTheme || enabledTheme === UiThemeEnum.Automatic) {
|
||||||
|
enabledTheme = window.matchMedia(QUERY_MEDIA_COLOR_LIGHT).matches
|
||||||
|
? UiThemeEnum.Light
|
||||||
|
: UiThemeEnum.Dark;
|
||||||
|
}
|
||||||
|
return rawPath.replaceAll("%(theme)s", enabledTheme);
|
||||||
|
}
|
@ -11,6 +11,7 @@ import { WebsocketClient } from "@goauthentik/common/ws";
|
|||||||
import { Interface } from "@goauthentik/elements/Interface";
|
import { Interface } from "@goauthentik/elements/Interface";
|
||||||
import "@goauthentik/elements/LoadingOverlay";
|
import "@goauthentik/elements/LoadingOverlay";
|
||||||
import "@goauthentik/elements/ak-locale-context";
|
import "@goauthentik/elements/ak-locale-context";
|
||||||
|
import { themeImage } from "@goauthentik/elements/utils/images";
|
||||||
import "@goauthentik/flow/sources/apple/AppleLoginInit";
|
import "@goauthentik/flow/sources/apple/AppleLoginInit";
|
||||||
import "@goauthentik/flow/sources/plex/PlexLoginInit";
|
import "@goauthentik/flow/sources/plex/PlexLoginInit";
|
||||||
import "@goauthentik/flow/stages/FlowErrorStage";
|
import "@goauthentik/flow/stages/FlowErrorStage";
|
||||||
@ -442,7 +443,9 @@ export class FlowExecutor extends Interface implements StageHost {
|
|||||||
renderChallengeWrapper(): TemplateResult {
|
renderChallengeWrapper(): TemplateResult {
|
||||||
const logo = html`<div class="pf-c-login__main-header pf-c-brand ak-brand">
|
const logo = html`<div class="pf-c-login__main-header pf-c-brand ak-brand">
|
||||||
<img
|
<img
|
||||||
src="${first(this.brand?.brandingLogo, globalAK()?.brand.brandingLogo, "")}"
|
src="${themeImage(
|
||||||
|
first(this.brand?.brandingLogo, globalAK()?.brand.brandingLogo, ""),
|
||||||
|
)}"
|
||||||
alt="authentik Logo"
|
alt="authentik Logo"
|
||||||
/>
|
/>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
@ -51,11 +51,6 @@ export class AutosubmitStage extends BaseStage<
|
|||||||
/>`;
|
/>`;
|
||||||
})}
|
})}
|
||||||
<ak-empty-state ?loading="${true}"> </ak-empty-state>
|
<ak-empty-state ?loading="${true}"> </ak-empty-state>
|
||||||
<div class="pf-c-form__group pf-m-action">
|
|
||||||
<button type="submit" class="pf-c-button pf-m-primary pf-m-block">
|
|
||||||
${msg("Continue")}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<footer class="pf-c-login__main-footer">
|
<footer class="pf-c-login__main-footer">
|
||||||
|
@ -5,6 +5,7 @@ import { first, getCookie } from "@goauthentik/common/utils";
|
|||||||
import { Interface } from "@goauthentik/elements/Interface";
|
import { Interface } from "@goauthentik/elements/Interface";
|
||||||
import "@goauthentik/elements/ak-locale-context";
|
import "@goauthentik/elements/ak-locale-context";
|
||||||
import { DefaultBrand } from "@goauthentik/elements/sidebar/SidebarBrand";
|
import { DefaultBrand } from "@goauthentik/elements/sidebar/SidebarBrand";
|
||||||
|
import { themeImage } from "@goauthentik/elements/utils/images";
|
||||||
import "rapidoc";
|
import "rapidoc";
|
||||||
|
|
||||||
import { CSSResult, TemplateResult, css, html } from "lit";
|
import { CSSResult, TemplateResult, css, html } from "lit";
|
||||||
@ -103,7 +104,9 @@ export class APIBrowser extends Interface {
|
|||||||
<img
|
<img
|
||||||
alt="authentik Logo"
|
alt="authentik Logo"
|
||||||
class="logo"
|
class="logo"
|
||||||
src="${first(this.brand?.brandingLogo, DefaultBrand.brandingLogo)}"
|
src="${themeImage(
|
||||||
|
first(this.brand?.brandingLogo, DefaultBrand.brandingLogo),
|
||||||
|
)}"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</rapi-doc>
|
</rapi-doc>
|
||||||
|
@ -21,6 +21,7 @@ import "@goauthentik/elements/router/RouterOutlet";
|
|||||||
import "@goauthentik/elements/sidebar/Sidebar";
|
import "@goauthentik/elements/sidebar/Sidebar";
|
||||||
import { DefaultBrand } from "@goauthentik/elements/sidebar/SidebarBrand";
|
import { DefaultBrand } from "@goauthentik/elements/sidebar/SidebarBrand";
|
||||||
import "@goauthentik/elements/sidebar/SidebarItem";
|
import "@goauthentik/elements/sidebar/SidebarItem";
|
||||||
|
import { themeImage } from "@goauthentik/elements/utils/images";
|
||||||
import { ROUTES } from "@goauthentik/user/Routes";
|
import { ROUTES } from "@goauthentik/user/Routes";
|
||||||
import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
|
import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
|
||||||
import { match } from "ts-pattern";
|
import { match } from "ts-pattern";
|
||||||
@ -193,7 +194,7 @@ class UserInterfacePresentation extends AKElement {
|
|||||||
<a href="#/" class="pf-c-page__header-brand-link">
|
<a href="#/" class="pf-c-page__header-brand-link">
|
||||||
<img
|
<img
|
||||||
class="pf-c-brand"
|
class="pf-c-brand"
|
||||||
src="${this.brand.brandingLogo}"
|
src="${themeImage(this.brand.brandingLogo)}"
|
||||||
alt="${this.brand.brandingTitle}"
|
alt="${this.brand.brandingTitle}"
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
|
@ -20,3 +20,7 @@ This means that if you want to select a default flow based on policy, you can le
|
|||||||
## Branding
|
## Branding
|
||||||
|
|
||||||
The brand configuration controls the branding title (shown in website document title and several other places), and the sidebar/header logo that appears in the upper left of the product interface.
|
The brand configuration controls the branding title (shown in website document title and several other places), and the sidebar/header logo that appears in the upper left of the product interface.
|
||||||
|
|
||||||
|
:::info
|
||||||
|
Starting with authentik 2024.6.2, the placeholder `%(theme)s` can be used in the logo configuration option, which will be replaced with the active theme.
|
||||||
|
:::
|
||||||
|
@ -3,6 +3,11 @@
|
|||||||
# Allowed levels: trace, debug, info, warning, error
|
# Allowed levels: trace, debug, info, warning, error
|
||||||
# Applies to: non-embedded
|
# Applies to: non-embedded
|
||||||
log_level: debug
|
log_level: debug
|
||||||
|
# Interval at which the outpost will refresh the providers
|
||||||
|
# from authentik. For caching outposts (such as LDAP), the
|
||||||
|
# cache will also be invalidated at that interval.
|
||||||
|
# (Format: hours=1;minutes=2;seconds=3).
|
||||||
|
refresh_interval: minutes=5
|
||||||
########################################
|
########################################
|
||||||
# The settings below are only relevant when using a managed outpost
|
# The settings below are only relevant when using a managed outpost
|
||||||
########################################
|
########################################
|
||||||
|
31
website/docs/security/CVE-2024-42490.md
Normal file
31
website/docs/security/CVE-2024-42490.md
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# CVE-2024-42490
|
||||||
|
|
||||||
|
_Reported by [@m2a2](https://github.com/m2a2)_
|
||||||
|
|
||||||
|
## Insufficient Authorization for several API endpoints
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
|
||||||
|
Several API endpoints can be accessed by users without correct authentication/authorization.
|
||||||
|
|
||||||
|
The main API endpoints affected by this:
|
||||||
|
|
||||||
|
- `/api/v3/crypto/certificatekeypairs/<uuid>/view_certificate/`
|
||||||
|
- `/api/v3/crypto/certificatekeypairs/<uuid>/view_private_key/`
|
||||||
|
- `/api/v3/.../used_by/`
|
||||||
|
|
||||||
|
Note that all of the affected API endpoints require the knowledge of the ID of an object, which especially for certificates is not accessible to an unprivileged user. Additionally the IDs for most objects are UUIDv4, meaning they are not easily guessable/enumerable.
|
||||||
|
|
||||||
|
### Patches
|
||||||
|
|
||||||
|
authentik 2024.4.4, 2024.6.4 and 2024.8.0 fix this issue.
|
||||||
|
|
||||||
|
### Workarounds
|
||||||
|
|
||||||
|
Access to the API endpoints can be blocked at a Reverse-proxy/Load balancer level to prevent this issue from being exploited.
|
||||||
|
|
||||||
|
### For more information
|
||||||
|
|
||||||
|
If you have any questions or comments about this advisory:
|
||||||
|
|
||||||
|
- Email us at [security@goauthentik.io](mailto:security@goauthentik.io)
|
35
website/docs/security/CVE-2024-47070.md
Normal file
35
website/docs/security/CVE-2024-47070.md
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
# CVE-2024-47070
|
||||||
|
|
||||||
|
_Reported by [@efpi-bot](https://github.com/efpi-bot) from [LogicalTrust](https://logicaltrust.net/en/)_
|
||||||
|
|
||||||
|
## Password authentication bypass via X-Forwarded-For HTTP header
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
|
||||||
|
The vulnerability allows bypassing policies by adding X-Forwarded-For header with unparsable IP address, e.g. "a". This results in a possibility to authenticate/authorize to any account with known login or email address.
|
||||||
|
|
||||||
|
Since the default authentication flow uses a policy to enable the password stage only when there is no password stage selected on the Identification stage, this vulnerability can be used to skip this policy and continue without the password stage.
|
||||||
|
|
||||||
|
### Am I affected
|
||||||
|
|
||||||
|
This can be exploited for the following configurations:
|
||||||
|
|
||||||
|
- An attacker can access authentik without a reverse proxy (and `AUTHENTIK_LISTEN__TRUSTED_PROXY_CIDRS` is not configured properly)
|
||||||
|
- The reverse proxy configuration does not correctly overwrite X-Forwarded-For
|
||||||
|
- Policies (User and group bindings do _not_ apply) are bound to authentication/authorization flows
|
||||||
|
|
||||||
|
### Patches
|
||||||
|
|
||||||
|
authentik 2024.6.5 and 2024.8.3 fix this issue.
|
||||||
|
|
||||||
|
### Workarounds
|
||||||
|
|
||||||
|
Ensure the X-Forwarded-For header is always set by the reverse proxy, and is always set to a correct IP.
|
||||||
|
|
||||||
|
In addition you can manually change the _Failure result_ option on policy bindings to _Pass_, which will prevent any stages from being skipped if a malicious request is received.
|
||||||
|
|
||||||
|
### For more information
|
||||||
|
|
||||||
|
If you have any questions or comments about this advisory:
|
||||||
|
|
||||||
|
- Email us at [security@goauthentik.io](mailto:security@goauthentik.io)
|
25
website/docs/security/CVE-2024-47077.md
Normal file
25
website/docs/security/CVE-2024-47077.md
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
# CVE-2024-47077
|
||||||
|
|
||||||
|
_Reported by [@quentinmit](https://github.com/quentinmit)_
|
||||||
|
|
||||||
|
## Insufficient cross-provider token validation during introspection
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
|
||||||
|
Access tokens issued to one application can be stolen by that application and used to impersonate the user against any other proxy provider. Also, a user can steal an access token they were legitimately issued for one application and use it to access another application that they aren't allowed to access.
|
||||||
|
|
||||||
|
### Details
|
||||||
|
|
||||||
|
The proxy provider uses `/application/o/introspect/` to validate bearer tokens provided in the `Authorization` header:
|
||||||
|
|
||||||
|
The implementation of this endpoint separately validates the `client_id` and `client_secret` (which are that of the proxy provider) and the `token` without validating that they correspond to the same provider.
|
||||||
|
|
||||||
|
### Patches
|
||||||
|
|
||||||
|
authentik 2024.6.5 and 2024.8.3 fix this issue.
|
||||||
|
|
||||||
|
### For more information
|
||||||
|
|
||||||
|
If you have any questions or comments about this advisory:
|
||||||
|
|
||||||
|
- Email us at [security@goauthentik.io](mailto:security@goauthentik.io)
|
@ -511,6 +511,9 @@ const docsSidebar = {
|
|||||||
items: [
|
items: [
|
||||||
"security/security-hardening",
|
"security/security-hardening",
|
||||||
"security/policy",
|
"security/policy",
|
||||||
|
"security/CVE-2024-47077",
|
||||||
|
"security/CVE-2024-47070",
|
||||||
|
"security/CVE-2024-42490",
|
||||||
"security/CVE-2024-38371",
|
"security/CVE-2024-38371",
|
||||||
"security/CVE-2024-37905",
|
"security/CVE-2024-37905",
|
||||||
"security/CVE-2024-23647",
|
"security/CVE-2024-23647",
|
||||||
|
Reference in New Issue
Block a user