mirror of
https://github.com/goauthentik/authentik
synced 2026-05-07 15:42:48 +02:00
Compare commits
56 Commits
pf5-fonts
...
language-c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4bd7464bd8 | ||
|
|
f914af70f1 | ||
|
|
a46c5e5d89 | ||
|
|
1ebd7cf0f4 | ||
|
|
5a7a76f9b3 | ||
|
|
f607378487 | ||
|
|
e114a68505 | ||
|
|
ebe028f3c9 | ||
|
|
eb60276846 | ||
|
|
952a0f796d | ||
|
|
b7205ff167 | ||
|
|
5379c509d6 | ||
|
|
63119df516 | ||
|
|
cd53ab5eba | ||
|
|
5aa7a1c62d | ||
|
|
092c8a3db4 | ||
|
|
9d8427b1b4 | ||
|
|
92556ca783 | ||
|
|
d5c2aedc8e | ||
|
|
02304081ed | ||
|
|
d60b6faa61 | ||
|
|
b12d1ed410 | ||
|
|
1276d87d69 | ||
|
|
0ba097ad95 | ||
|
|
3ff7332742 | ||
|
|
72f0f98706 | ||
|
|
873a47f9a2 | ||
|
|
382bd324c2 | ||
|
|
71e57611d0 | ||
|
|
04a8f02b2a | ||
|
|
2250cea934 | ||
|
|
b6d21bad71 | ||
|
|
2bb86f6f12 | ||
|
|
c75ff13770 | ||
|
|
874a20b908 | ||
|
|
ea7cbafefb | ||
|
|
46b889eab1 | ||
|
|
c0744d6cf3 | ||
|
|
9bae5caee4 | ||
|
|
5756b189bb | ||
|
|
af2f86e030 | ||
|
|
be90f63f43 | ||
|
|
7357b9cc58 | ||
|
|
ec467bc2d1 | ||
|
|
d140e4fdd3 | ||
|
|
75ead31448 | ||
|
|
713bb04c21 | ||
|
|
c14a029b23 | ||
|
|
6092a26450 | ||
|
|
6fba028404 | ||
|
|
678e1d87ba | ||
|
|
f1a1f327cd | ||
|
|
d94117a8e3 | ||
|
|
fa32567230 | ||
|
|
59da20e81c | ||
|
|
1fb71371cb |
1
.github/transifex.yml
vendored
1
.github/transifex.yml
vendored
@@ -1,3 +1,4 @@
|
||||
---
|
||||
git:
|
||||
filters:
|
||||
- filter_type: file
|
||||
|
||||
2
.github/workflows/release-tag.yml
vendored
2
.github/workflows/release-tag.yml
vendored
@@ -89,7 +89,7 @@ jobs:
|
||||
git tag "version/${{ inputs.version }}" HEAD -m "version/${{ inputs.version }}"
|
||||
git push --follow-tags
|
||||
- name: Create Release
|
||||
uses: softprops/action-gh-release@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe # v2.4.2
|
||||
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
|
||||
with:
|
||||
token: "${{ steps.app-token.outputs.token }}"
|
||||
tag_name: "version/${{ inputs.version }}"
|
||||
|
||||
41
.github/workflows/translation-rename.yml
vendored
41
.github/workflows/translation-rename.yml
vendored
@@ -1,41 +0,0 @@
|
||||
---
|
||||
# Rename transifex pull requests to have a correct naming
|
||||
# Also enables auto squash-merge
|
||||
name: Translation - Auto-rename Transifex PRs
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, reopened]
|
||||
|
||||
permissions:
|
||||
# Permission to rename PR
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
rename_pr:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.pull_request.user.login == 'transifex-integration[bot]'}}
|
||||
steps:
|
||||
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
|
||||
- id: generate_token
|
||||
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2
|
||||
with:
|
||||
app-id: ${{ secrets.GH_APP_ID }}
|
||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
- name: Get current title
|
||||
id: title
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.generate_token.outputs.token }}
|
||||
run: |
|
||||
title=$(gh pr view ${{ github.event.pull_request.number }} --json "title" -q ".title")
|
||||
echo "title=${title}" >> "$GITHUB_OUTPUT"
|
||||
- name: Rename
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.generate_token.outputs.token }}
|
||||
run: |
|
||||
gh pr edit ${{ github.event.pull_request.number }} -t "translate: ${{ steps.title.outputs.title }}" --add-label dependencies
|
||||
- uses: peter-evans/enable-pull-request-automerge@a660677d5469627102a1c1e11409dd063606628d # v3
|
||||
with:
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
pull-request-number: ${{ github.event.pull_request.number }}
|
||||
merge-method: squash
|
||||
@@ -76,7 +76,7 @@ RUN --mount=type=secret,id=GEOIPUPDATE_ACCOUNT_ID \
|
||||
/bin/sh -c "GEOIPUPDATE_LICENSE_KEY_FILE=/run/secrets/GEOIPUPDATE_LICENSE_KEY /usr/bin/entry.sh || echo 'Failed to get GeoIP database, disabling'; exit 0"
|
||||
|
||||
# Stage 4: Download uv
|
||||
FROM ghcr.io/astral-sh/uv:0.9.13@sha256:f07d1bf7b1fb4b983eed2b31320e25a2a76625bdf83d5ff0208fe105d4d8d2f5 AS uv
|
||||
FROM ghcr.io/astral-sh/uv:0.9.14@sha256:fef8e5fb8809f4b57069e919ffcd1529c92b432a2c8d8ad1768087b0b018d840 AS uv
|
||||
# Stage 5: Base python image
|
||||
FROM ghcr.io/goauthentik/fips-python:3.13.9-slim-trixie-fips@sha256:700fc8c1e290bd14e5eaca50b1d8e8c748c820010559cbfb4c4f8dfbe2c4c9ff AS python-base
|
||||
|
||||
|
||||
@@ -27,16 +27,16 @@ except OSError:
|
||||
ipc_key = None
|
||||
|
||||
|
||||
def validate_auth(header: bytes) -> str | None:
|
||||
def validate_auth(header: bytes, format="bearer") -> str | None:
|
||||
"""Validate that the header is in a correct format,
|
||||
returns type and credentials"""
|
||||
auth_credentials = header.decode().strip()
|
||||
if auth_credentials == "" or " " not in auth_credentials:
|
||||
return None
|
||||
auth_type, _, auth_credentials = auth_credentials.partition(" ")
|
||||
if auth_type.lower() != "bearer":
|
||||
if not compare_digest(auth_type.lower(), format):
|
||||
LOGGER.debug("Unsupported authentication type, denying", type=auth_type.lower())
|
||||
raise AuthenticationFailed("Unsupported authentication type")
|
||||
return None
|
||||
if auth_credentials == "": # nosec # noqa
|
||||
raise AuthenticationFailed("Malformed header")
|
||||
return auth_credentials
|
||||
|
||||
@@ -24,8 +24,7 @@ class TestAPIAuth(TestCase):
|
||||
|
||||
def test_invalid_type(self):
|
||||
"""Test invalid type"""
|
||||
with self.assertRaises(AuthenticationFailed):
|
||||
bearer_auth(b"foo bar")
|
||||
self.assertIsNone(bearer_auth(b"foo bar"))
|
||||
|
||||
def test_invalid_empty(self):
|
||||
"""Test invalid type"""
|
||||
@@ -34,9 +33,8 @@ class TestAPIAuth(TestCase):
|
||||
|
||||
def test_invalid_no_token(self):
|
||||
"""Test invalid with no token"""
|
||||
with self.assertRaises(AuthenticationFailed):
|
||||
auth = b64encode(b":abc").decode()
|
||||
self.assertIsNone(bearer_auth(f"Basic :{auth}".encode()))
|
||||
auth = b64encode(b":abc").decode()
|
||||
self.assertIsNone(bearer_auth(f"Basic :{auth}".encode()))
|
||||
|
||||
def test_bearer_valid(self):
|
||||
"""Test valid token"""
|
||||
|
||||
@@ -5,6 +5,7 @@ from pathlib import Path
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
from django.dispatch import Signal
|
||||
from django.http import HttpRequest
|
||||
from drf_spectacular.utils import extend_schema
|
||||
from rest_framework.fields import (
|
||||
BooleanField,
|
||||
@@ -63,7 +64,8 @@ class ConfigView(APIView):
|
||||
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
def get_capabilities(self) -> list[Capabilities]:
|
||||
@staticmethod
|
||||
def get_capabilities(request: HttpRequest) -> list[Capabilities]:
|
||||
"""Get all capabilities this server instance supports"""
|
||||
caps = []
|
||||
deb_test = settings.DEBUG or settings.TEST
|
||||
@@ -76,18 +78,19 @@ class ConfigView(APIView):
|
||||
for processor in get_context_processors():
|
||||
if cap := processor.capability():
|
||||
caps.append(cap)
|
||||
if self.request.tenant.impersonation:
|
||||
if request.tenant.impersonation:
|
||||
caps.append(Capabilities.CAN_IMPERSONATE)
|
||||
if settings.DEBUG: # pragma: no cover
|
||||
caps.append(Capabilities.CAN_DEBUG)
|
||||
if "authentik.enterprise" in settings.INSTALLED_APPS:
|
||||
caps.append(Capabilities.IS_ENTERPRISE)
|
||||
for _, result in capabilities.send(sender=self):
|
||||
for _, result in capabilities.send(sender=ConfigView):
|
||||
if result:
|
||||
caps.append(result)
|
||||
return caps
|
||||
|
||||
def get_config(self) -> ConfigSerializer:
|
||||
@staticmethod
|
||||
def get_config(request: HttpRequest) -> ConfigSerializer:
|
||||
"""Get Config"""
|
||||
return ConfigSerializer(
|
||||
{
|
||||
@@ -98,7 +101,7 @@ class ConfigView(APIView):
|
||||
"send_pii": CONFIG.get("error_reporting.send_pii"),
|
||||
"traces_sample_rate": float(CONFIG.get("error_reporting.sample_rate", 0.4)),
|
||||
},
|
||||
"capabilities": self.get_capabilities(),
|
||||
"capabilities": ConfigView.get_capabilities(request),
|
||||
"cache_timeout": CONFIG.get_int("cache.timeout"),
|
||||
"cache_timeout_flows": CONFIG.get_int("cache.timeout_flows"),
|
||||
"cache_timeout_policies": CONFIG.get_int("cache.timeout_policies"),
|
||||
@@ -108,4 +111,4 @@ class ConfigView(APIView):
|
||||
@extend_schema(responses={200: ConfigSerializer(many=False)})
|
||||
def get(self, request: Request) -> Response:
|
||||
"""Retrieve public configuration options"""
|
||||
return Response(self.get_config().data)
|
||||
return Response(ConfigView.get_config(request).data)
|
||||
|
||||
@@ -44,6 +44,8 @@ from authentik.core.models import (
|
||||
)
|
||||
from authentik.endpoints.connectors.agent.models import (
|
||||
AgentDeviceConnection,
|
||||
AppleNonce,
|
||||
DeviceAuthenticationToken,
|
||||
)
|
||||
from authentik.endpoints.connectors.agent.models import (
|
||||
DeviceToken as EndpointDeviceToken,
|
||||
@@ -150,6 +152,8 @@ def excluded_models() -> list[type[Model]]:
|
||||
EndpointDeviceToken,
|
||||
Device,
|
||||
DeviceConnection,
|
||||
DeviceAuthenticationToken,
|
||||
AppleNonce,
|
||||
AgentDeviceConnection,
|
||||
DeviceFactSnapshot,
|
||||
DeviceToken,
|
||||
|
||||
@@ -14,6 +14,7 @@ from drf_spectacular.utils import (
|
||||
extend_schema_field,
|
||||
)
|
||||
from guardian.shortcuts import get_objects_for_user
|
||||
from rest_framework.authentication import SessionAuthentication
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.fields import CharField, IntegerField, SerializerMethodField
|
||||
from rest_framework.request import Request
|
||||
@@ -22,10 +23,12 @@ from rest_framework.serializers import ListSerializer, ValidationError
|
||||
from rest_framework.validators import UniqueValidator
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.api.authentication import TokenAuthentication
|
||||
from authentik.api.validation import validate
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.core.api.utils import JSONDictField, ModelSerializer, PassiveSerializer
|
||||
from authentik.core.models import Group, User
|
||||
from authentik.endpoints.connectors.agent.auth import AgentAuth
|
||||
from authentik.rbac.api.roles import RoleSerializer
|
||||
from authentik.rbac.decorators import permission_required
|
||||
|
||||
@@ -228,6 +231,11 @@ class GroupViewSet(UsedByMixin, ModelViewSet):
|
||||
search_fields = ["name", "is_superuser"]
|
||||
filterset_class = GroupFilter
|
||||
ordering = ["name"]
|
||||
authentication_classes = [
|
||||
TokenAuthentication,
|
||||
SessionAuthentication,
|
||||
AgentAuth,
|
||||
]
|
||||
|
||||
def get_ql_fields(self):
|
||||
from djangoql.schema import BoolField, StrField
|
||||
|
||||
@@ -109,6 +109,8 @@ class TokenSerializer(ManagedSerializer, ModelSerializer):
|
||||
|
||||
|
||||
class TokenSetKeySerializer(PassiveSerializer):
|
||||
"""Set token's key"""
|
||||
|
||||
key = CharField()
|
||||
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ from drf_spectacular.utils import (
|
||||
inline_serializer,
|
||||
)
|
||||
from guardian.shortcuts import get_objects_for_user
|
||||
from rest_framework.authentication import SessionAuthentication
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.fields import (
|
||||
@@ -52,6 +53,7 @@ from rest_framework.validators import UniqueValidator
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.api.authentication import TokenAuthentication
|
||||
from authentik.api.validation import validate
|
||||
from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT
|
||||
from authentik.brands.models import Brand
|
||||
@@ -76,6 +78,7 @@ from authentik.core.models import (
|
||||
User,
|
||||
UserTypes,
|
||||
)
|
||||
from authentik.endpoints.connectors.agent.auth import AgentAuth
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.flows.exceptions import FlowNonApplicableException
|
||||
from authentik.flows.models import FlowToken
|
||||
@@ -433,6 +436,11 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
||||
serializer_class = UserSerializer
|
||||
filterset_class = UsersFilter
|
||||
search_fields = ["email", "name", "uuid", "username"]
|
||||
authentication_classes = [
|
||||
TokenAuthentication,
|
||||
SessionAuthentication,
|
||||
AgentAuth,
|
||||
]
|
||||
|
||||
def get_ql_fields(self):
|
||||
from djangoql.schema import BoolField, StrField
|
||||
|
||||
@@ -8,7 +8,6 @@ from django.http.response import HttpResponse
|
||||
from django.shortcuts import redirect
|
||||
from django.utils.translation import gettext as _
|
||||
from django.views.generic.base import RedirectView, TemplateView
|
||||
from rest_framework.request import Request
|
||||
|
||||
from authentik import authentik_build_hash
|
||||
from authentik.admin.tasks import LOCAL_VERSION
|
||||
@@ -47,7 +46,7 @@ class InterfaceView(TemplateView):
|
||||
|
||||
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
|
||||
brand = CurrentBrandSerializer(self.request.brand)
|
||||
kwargs["config_json"] = dumps(ConfigView(request=Request(self.request)).get_config().data)
|
||||
kwargs["config_json"] = dumps(ConfigView.get_config(self.request).data)
|
||||
kwargs["ui_theme"] = brand.data["ui_theme"]
|
||||
kwargs["brand_json"] = dumps(brand.data)
|
||||
kwargs["version_family"] = f"{LOCAL_VERSION.major}.{LOCAL_VERSION.minor}"
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
"""authentik crypto app config"""
|
||||
|
||||
from datetime import UTC, datetime
|
||||
|
||||
from dramatiq.broker import get_broker
|
||||
|
||||
from authentik.blueprints.apps import ManagedAppConfig
|
||||
@@ -47,10 +45,7 @@ class AuthentikCryptoConfig(ManagedAppConfig):
|
||||
cert: CertificateKeyPair | None = CertificateKeyPair.objects.filter(
|
||||
managed=MANAGED_KEY
|
||||
).first()
|
||||
now = datetime.now(tz=UTC)
|
||||
if not cert or (
|
||||
now < cert.certificate.not_valid_after_utc or now > cert.certificate.not_valid_after_utc
|
||||
):
|
||||
if not cert:
|
||||
self._create_update_cert()
|
||||
|
||||
@ManagedAppConfig.reconcile_tenant
|
||||
|
||||
@@ -2,24 +2,24 @@ from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.core.api.utils import ModelSerializer
|
||||
from authentik.endpoints.models import DeviceGroup
|
||||
from authentik.endpoints.models import DeviceAccessGroup
|
||||
|
||||
|
||||
class DeviceGroupSerializer(ModelSerializer):
|
||||
class DeviceAccessGroupSerializer(ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = DeviceGroup
|
||||
model = DeviceAccessGroup
|
||||
fields = [
|
||||
"pbm_uuid",
|
||||
"name",
|
||||
]
|
||||
|
||||
|
||||
class DeviceGroupViewSet(UsedByMixin, ModelViewSet):
|
||||
"""DeviceGroup Viewset"""
|
||||
class DeviceAccessGroupViewSet(UsedByMixin, ModelViewSet):
|
||||
"""DeviceAccessGroup Viewset"""
|
||||
|
||||
queryset = DeviceGroup.objects.all()
|
||||
serializer_class = DeviceGroupSerializer
|
||||
queryset = DeviceAccessGroup.objects.all()
|
||||
serializer_class = DeviceAccessGroupSerializer
|
||||
search_fields = [
|
||||
"pbm_uuid",
|
||||
"name",
|
||||
@@ -4,15 +4,15 @@ from rest_framework.viewsets import GenericViewSet
|
||||
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.core.api.utils import ModelSerializer
|
||||
from authentik.endpoints.api.device_access_group import DeviceAccessGroupSerializer
|
||||
from authentik.endpoints.api.device_connections import DeviceConnectionSerializer
|
||||
from authentik.endpoints.api.device_fact_snapshots import DeviceFactSnapshotSerializer
|
||||
from authentik.endpoints.api.device_group import DeviceGroupSerializer
|
||||
from authentik.endpoints.models import Device
|
||||
|
||||
|
||||
class EndpointDeviceSerializer(ModelSerializer):
|
||||
|
||||
group_obj = DeviceGroupSerializer(source="group")
|
||||
access_group_obj = DeviceAccessGroupSerializer(source="access_group", required=False)
|
||||
|
||||
facts = SerializerMethodField()
|
||||
|
||||
@@ -25,8 +25,8 @@ class EndpointDeviceSerializer(ModelSerializer):
|
||||
"device_uuid",
|
||||
"pbm_uuid",
|
||||
"name",
|
||||
"group",
|
||||
"group_obj",
|
||||
"access_group",
|
||||
"access_group_obj",
|
||||
"expiring",
|
||||
"expires",
|
||||
"facts",
|
||||
@@ -58,7 +58,7 @@ class DeviceViewSet(
|
||||
GenericViewSet,
|
||||
):
|
||||
|
||||
queryset = Device.objects.all().select_related("group")
|
||||
queryset = Device.objects.all().select_related("access_group")
|
||||
serializer_class = EndpointDeviceSerializer
|
||||
search_fields = [
|
||||
"name",
|
||||
|
||||
66
authentik/endpoints/connectors/agent/api/agent.py
Normal file
66
authentik/endpoints/connectors/agent/api/agent.py
Normal file
@@ -0,0 +1,66 @@
|
||||
from rest_framework.fields import (
|
||||
BooleanField,
|
||||
CharField,
|
||||
IntegerField,
|
||||
SerializerMethodField,
|
||||
)
|
||||
|
||||
from authentik.api.v3.config import ConfigSerializer, ConfigView
|
||||
from authentik.core.api.utils import PassiveSerializer
|
||||
from authentik.crypto.apps import MANAGED_KEY
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.endpoints.connectors.agent.models import AgentConnector
|
||||
from authentik.endpoints.models import Device
|
||||
from authentik.lib.utils.time import timedelta_from_string
|
||||
from authentik.providers.oauth2.views.jwks import JWKSView
|
||||
|
||||
|
||||
class AgentConfigSerializer(PassiveSerializer):
|
||||
|
||||
device_id = SerializerMethodField()
|
||||
refresh_interval = SerializerMethodField()
|
||||
|
||||
authorization_flow = SerializerMethodField()
|
||||
jwks = SerializerMethodField()
|
||||
|
||||
nss_uid_offset = IntegerField()
|
||||
nss_gid_offset = IntegerField()
|
||||
auth_terminate_session_on_expiry = BooleanField()
|
||||
|
||||
system_config = SerializerMethodField()
|
||||
|
||||
def get_device_id(self, instance: AgentConnector) -> str:
|
||||
device: Device = self.context["device"]
|
||||
return device.pk
|
||||
|
||||
def get_refresh_interval(self, instance: AgentConnector) -> int:
|
||||
return int(timedelta_from_string(instance.refresh_interval).total_seconds())
|
||||
|
||||
def get_authorization_flow(self, instance: AgentConnector) -> str | None:
|
||||
if not instance.authorization_flow:
|
||||
return None
|
||||
return instance.authorization_flow.slug
|
||||
|
||||
def get_jwks(self, instance: AgentConnector) -> dict:
|
||||
kp = CertificateKeyPair.objects.filter(managed=MANAGED_KEY).first()
|
||||
return {"keys": [JWKSView.get_jwk_for_key(kp, "sig")]}
|
||||
|
||||
def get_system_config(self, instance: AgentConnector) -> ConfigSerializer:
|
||||
return ConfigView.get_config(self.context["request"]).data
|
||||
|
||||
|
||||
class EnrollSerializer(PassiveSerializer):
|
||||
|
||||
device_serial = CharField(required=True)
|
||||
device_name = CharField(required=True)
|
||||
|
||||
|
||||
class AgentTokenResponseSerializer(PassiveSerializer):
|
||||
|
||||
token = CharField(required=True)
|
||||
expires_in = IntegerField(required=0)
|
||||
|
||||
|
||||
class AgentAuthenticationResponse(PassiveSerializer):
|
||||
|
||||
url = CharField()
|
||||
@@ -6,11 +6,8 @@ from drf_spectacular.utils import OpenApiResponse, extend_schema
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import PermissionDenied, ValidationError
|
||||
from rest_framework.fields import (
|
||||
BooleanField,
|
||||
CharField,
|
||||
ChoiceField,
|
||||
IntegerField,
|
||||
SerializerMethodField,
|
||||
)
|
||||
from rest_framework.relations import PrimaryKeyRelatedField
|
||||
from rest_framework.request import Request
|
||||
@@ -20,6 +17,11 @@ from rest_framework.viewsets import ModelViewSet
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.core.api.utils import PassiveSerializer
|
||||
from authentik.endpoints.api.connectors import ConnectorSerializer
|
||||
from authentik.endpoints.connectors.agent.api.agent import (
|
||||
AgentConfigSerializer,
|
||||
AgentTokenResponseSerializer,
|
||||
EnrollSerializer,
|
||||
)
|
||||
from authentik.endpoints.connectors.agent.auth import (
|
||||
AgentAuth,
|
||||
AgentEnrollmentAuth,
|
||||
@@ -32,37 +34,24 @@ from authentik.endpoints.connectors.agent.models import (
|
||||
)
|
||||
from authentik.endpoints.facts import DeviceFacts, OSFamily
|
||||
from authentik.endpoints.models import Device
|
||||
from authentik.lib.utils.time import timedelta_from_string
|
||||
from authentik.lib.utils.reflection import ConditionalInheritance
|
||||
|
||||
|
||||
class AgentConnectorSerializer(ConnectorSerializer):
|
||||
|
||||
class Meta(ConnectorSerializer.Meta):
|
||||
model = AgentConnector
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class AgentConfigSerializer(PassiveSerializer):
|
||||
|
||||
nss_uid_offset = IntegerField()
|
||||
nss_gid_offset = IntegerField()
|
||||
authentication_flow = CharField()
|
||||
auth_terminate_session_on_expiry = BooleanField()
|
||||
refresh_interval = SerializerMethodField()
|
||||
|
||||
def get_refresh_interval(self, instance: AgentConnector) -> int:
|
||||
return int(timedelta_from_string(instance.refresh_interval).total_seconds())
|
||||
|
||||
|
||||
class EnrollSerializer(PassiveSerializer):
|
||||
|
||||
device_serial = CharField()
|
||||
device_name = CharField()
|
||||
|
||||
|
||||
class EnrollResponseSerializer(PassiveSerializer):
|
||||
|
||||
token = CharField()
|
||||
fields = ConnectorSerializer.Meta.fields + [
|
||||
"snapshot_expiry",
|
||||
"auth_session_duration",
|
||||
"auth_terminate_session_on_expiry",
|
||||
"refresh_interval",
|
||||
"authorization_flow",
|
||||
"nss_uid_offset",
|
||||
"nss_gid_offset",
|
||||
"challenge_key",
|
||||
"jwt_federation_providers",
|
||||
]
|
||||
|
||||
|
||||
class MDMConfigSerializer(PassiveSerializer):
|
||||
@@ -88,7 +77,13 @@ class MDMConfigResponseSerializer(PassiveSerializer):
|
||||
config = CharField(required=True)
|
||||
|
||||
|
||||
class AgentConnectorViewSet(UsedByMixin, ModelViewSet):
|
||||
class AgentConnectorViewSet(
|
||||
ConditionalInheritance(
|
||||
"authentik.enterprise.endpoints.connectors.agent.api.connectors.AgentConnectorViewSetMixin"
|
||||
),
|
||||
UsedByMixin,
|
||||
ModelViewSet,
|
||||
):
|
||||
|
||||
queryset = AgentConnector.objects.all()
|
||||
serializer_class = AgentConnectorSerializer
|
||||
@@ -96,61 +91,6 @@ class AgentConnectorViewSet(UsedByMixin, ModelViewSet):
|
||||
ordering = ["name"]
|
||||
filterset_fields = ["name", "enabled"]
|
||||
|
||||
@extend_schema(
|
||||
request=EnrollSerializer(),
|
||||
responses={200: EnrollResponseSerializer},
|
||||
)
|
||||
@action(
|
||||
methods=["POST"],
|
||||
detail=False,
|
||||
authentication_classes=[AgentEnrollmentAuth],
|
||||
)
|
||||
def enroll(self, request: Request):
|
||||
token: EnrollmentToken = request.auth
|
||||
data = EnrollSerializer(data=request.data)
|
||||
data.is_valid(raise_exception=True)
|
||||
device, _ = Device.objects.get_or_create(
|
||||
identifier=data.validated_data["device_serial"],
|
||||
defaults={
|
||||
"name": data.validated_data["device_name"],
|
||||
"expiring": False,
|
||||
"group": token.device_group,
|
||||
},
|
||||
)
|
||||
connection, _ = AgentDeviceConnection.objects.update_or_create(
|
||||
device=device,
|
||||
connector=token.connector,
|
||||
)
|
||||
token = DeviceToken.objects.create(device=connection, expiring=False)
|
||||
return Response(
|
||||
{
|
||||
"token": token.key,
|
||||
}
|
||||
)
|
||||
|
||||
@extend_schema(
|
||||
responses=AgentConfigSerializer(),
|
||||
request=OpenApiTypes.NONE,
|
||||
)
|
||||
@action(methods=["GET"], detail=False, authentication_classes=[AgentAuth])
|
||||
def agent_config(self, request: Request):
|
||||
token: DeviceToken = request.auth
|
||||
connector: AgentConnector = token.device.connector.agentconnector
|
||||
return Response(AgentConfigSerializer(connector).data)
|
||||
|
||||
@extend_schema(
|
||||
request=DeviceFacts(),
|
||||
responses={204: OpenApiResponse(description="Successfully checked in")},
|
||||
)
|
||||
@action(methods=["POST"], detail=False, authentication_classes=[AgentAuth])
|
||||
def check_in(self, request: Request):
|
||||
token: DeviceToken = request.auth
|
||||
data = DeviceFacts(data=request.data)
|
||||
data.is_valid(raise_exception=True)
|
||||
connection: AgentDeviceConnection = token.device
|
||||
connection.create_snapshot(data.validated_data)
|
||||
return Response(status=204)
|
||||
|
||||
@extend_schema(
|
||||
request=MDMConfigSerializer(),
|
||||
responses=MDMConfigResponseSerializer(),
|
||||
@@ -167,3 +107,63 @@ class AgentConnectorViewSet(UsedByMixin, ModelViewSet):
|
||||
ctrl = connector.controller(connector)
|
||||
payload = ctrl.generate_mdm_config(data.validated_data["platform"], request, token)
|
||||
return Response({"config": payload})
|
||||
|
||||
@extend_schema(
|
||||
request=EnrollSerializer(),
|
||||
responses={200: AgentTokenResponseSerializer},
|
||||
)
|
||||
@action(
|
||||
methods=["POST"],
|
||||
detail=False,
|
||||
authentication_classes=[AgentEnrollmentAuth],
|
||||
)
|
||||
def enroll(self, request: Request):
|
||||
token: EnrollmentToken = request.auth
|
||||
data = EnrollSerializer(data=request.data)
|
||||
data.is_valid(raise_exception=True)
|
||||
device, _ = Device.objects.get_or_create(
|
||||
identifier=data.validated_data["device_serial"],
|
||||
defaults={
|
||||
"name": data.validated_data["device_name"],
|
||||
"expiring": False,
|
||||
"access_group": token.device_group,
|
||||
},
|
||||
)
|
||||
connection, _ = AgentDeviceConnection.objects.update_or_create(
|
||||
device=device,
|
||||
connector=token.connector,
|
||||
)
|
||||
token = DeviceToken.objects.create(device=connection, expiring=False)
|
||||
return Response(
|
||||
{
|
||||
"token": token.key,
|
||||
"expires_in": 0,
|
||||
}
|
||||
)
|
||||
|
||||
@extend_schema(
|
||||
request=OpenApiTypes.NONE,
|
||||
responses=AgentConfigSerializer(),
|
||||
)
|
||||
@action(methods=["GET"], detail=False, authentication_classes=[AgentAuth])
|
||||
def agent_config(self, request: Request):
|
||||
token: DeviceToken = request.auth
|
||||
connector: AgentConnector = token.device.connector.agentconnector
|
||||
return Response(
|
||||
AgentConfigSerializer(
|
||||
connector, context={"request": request, "device": token.device.device}
|
||||
).data
|
||||
)
|
||||
|
||||
@extend_schema(
|
||||
request=DeviceFacts(),
|
||||
responses={204: OpenApiResponse(description="Successfully checked in")},
|
||||
)
|
||||
@action(methods=["POST"], detail=False, authentication_classes=[AgentAuth])
|
||||
def check_in(self, request: Request):
|
||||
token: DeviceToken = request.auth
|
||||
data = DeviceFacts(data=request.data)
|
||||
data.is_valid(raise_exception=True)
|
||||
connection: AgentDeviceConnection = token.device
|
||||
connection.create_snapshot(data.validated_data)
|
||||
return Response(status=204)
|
||||
|
||||
@@ -7,7 +7,7 @@ from rest_framework.viewsets import ModelViewSet
|
||||
from authentik.core.api.tokens import TokenViewSerializer
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.core.api.utils import ModelSerializer
|
||||
from authentik.endpoints.api.device_group import DeviceGroupSerializer
|
||||
from authentik.endpoints.api.device_access_group import DeviceAccessGroupSerializer
|
||||
from authentik.endpoints.connectors.agent.models import EnrollmentToken
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.rbac.decorators import permission_required
|
||||
@@ -15,7 +15,9 @@ from authentik.rbac.decorators import permission_required
|
||||
|
||||
class EnrollmentTokenSerializer(ModelSerializer):
|
||||
|
||||
device_group_obj = DeviceGroupSerializer(source="device_group", read_only=True, required=False)
|
||||
device_group_obj = DeviceAccessGroupSerializer(
|
||||
source="device_group", read_only=True, required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = EnrollmentToken
|
||||
|
||||
@@ -30,7 +30,9 @@ class AgentAuth(BaseAuthentication):
|
||||
|
||||
def authenticate(self, request: Request) -> tuple[User, Any] | None:
|
||||
auth = get_authorization_header(request)
|
||||
key = validate_auth(auth)
|
||||
key = validate_auth(auth, format="bearer+agent")
|
||||
if not key:
|
||||
return None
|
||||
device_token = DeviceToken.filter_not_expired(key=key).first()
|
||||
if not device_token:
|
||||
raise PermissionDenied()
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from plistlib import PlistFormat, dumps
|
||||
from uuid import uuid4
|
||||
from xml.etree.ElementTree import Element, SubElement, tostring # nosec
|
||||
|
||||
from django.http import HttpRequest
|
||||
@@ -63,18 +64,60 @@ class AgentConnectorController(BaseController[AgentConnector]):
|
||||
return payload
|
||||
|
||||
def _generate_mdm_config_macos(self, request: HttpRequest, token: EnrollmentToken) -> str:
|
||||
token_uuid = str(token.pk).upper()
|
||||
payload = dumps(
|
||||
{
|
||||
"PayloadContent": [
|
||||
# Config for authentik Platform Agent (sysd)
|
||||
{
|
||||
"PayloadDisplayName": "authentik Platform",
|
||||
"PayloadIdentifier": f"io.goauthentik.platform.{str(token.pk).upper()}",
|
||||
"PayloadIdentifier": f"io.goauthentik.platform.{token_uuid}",
|
||||
"PayloadType": "io.goauthentik.platform",
|
||||
"PayloadUUID": str(token.pk).upper(),
|
||||
"PayloadUUID": str(uuid4()),
|
||||
"PayloadVersion": 1,
|
||||
"RegistrationToken": token.key,
|
||||
"URL": request.build_absolute_uri(reverse("authentik_core:root-redirect")),
|
||||
}
|
||||
},
|
||||
# Config for MDM-associated domains (required for PSSO)
|
||||
{
|
||||
"PayloadDisplayName": "Associated Domains",
|
||||
"PayloadIdentifier": f"com.apple.associated-domains.{token_uuid}",
|
||||
"PayloadType": "com.apple.associated-domains",
|
||||
"PayloadUUID": str(uuid4()),
|
||||
"PayloadVersion": 1,
|
||||
"Configuration": [
|
||||
{
|
||||
"ApplicationIdentifier": "232G855Y8N.io.goauthentik.platform.agent",
|
||||
"AssociatedDomains": [f"authsrv:{request.get_host()}"],
|
||||
"EnableDirectDownloads": False,
|
||||
}
|
||||
],
|
||||
},
|
||||
# Config for Platform SSO
|
||||
{
|
||||
"PayloadDisplayName": "Platform Single Sign-On",
|
||||
"PayloadIdentifier": f"com.apple.extensiblesso.{token_uuid}",
|
||||
"PayloadType": "com.apple.extensiblesso",
|
||||
"PayloadUUID": str(uuid4()),
|
||||
"PayloadVersion": 1,
|
||||
"ExtensionIdentifier": "io.goauthentik.platform.psso",
|
||||
"TeamIdentifier": "232G855Y8N",
|
||||
"Type": "Redirect",
|
||||
"URLs": [request.build_absolute_uri("")],
|
||||
"PlatformSSO": {
|
||||
"AccountDisplayName": "authentik",
|
||||
"AllowDeviceIdentifiersInAttestation": True,
|
||||
"AuthenticationMethod": "UserSecureEnclaveKey",
|
||||
"EnableAuthorization": True,
|
||||
"EnableCreateUserAtLogin": True,
|
||||
"FileVaultPolicy": ["RequireAuthentication"],
|
||||
"LoginPolicy": ["RequireAuthentication"],
|
||||
"NewUserAuthorizationMode": "Standard",
|
||||
"UnlockPolicy": ["RequireAuthentication"],
|
||||
"UseSharedDeviceKeys": True,
|
||||
"UserAuthorizationMode": "Standard",
|
||||
},
|
||||
},
|
||||
],
|
||||
"PayloadDisplayName": "authentik Platform",
|
||||
"PayloadIdentifier": str(self.connector.pk).upper(),
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
# Generated by Django 5.2.8 on 2025-11-27 00:16
|
||||
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_endpoints", "0002_rename_devicegroup_deviceaccessgroup_and_more"),
|
||||
("authentik_endpoints_connectors_agent", "0001_initial"),
|
||||
(
|
||||
"authentik_providers_oauth2",
|
||||
"0031_remove_oauth2provider_backchannel_logout_uri_and_more",
|
||||
),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="DeviceAuthenticationToken",
|
||||
fields=[
|
||||
("expires", models.DateTimeField(default=None, null=True)),
|
||||
("expiring", models.BooleanField(default=True)),
|
||||
(
|
||||
"identifier",
|
||||
models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False),
|
||||
),
|
||||
("token", models.TextField()),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Device authentication token",
|
||||
"verbose_name_plural": "Device authentication tokens",
|
||||
"abstract": False,
|
||||
},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name="devicetoken",
|
||||
options={"verbose_name": "Device Token", "verbose_name_plural": "Device Tokens"},
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name="agentconnector",
|
||||
old_name="authentication_flow",
|
||||
new_name="authorization_flow",
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="agentconnector",
|
||||
name="jwt_federation_providers",
|
||||
field=models.ManyToManyField(
|
||||
blank=True, default=None, to="authentik_providers_oauth2.oauth2provider"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="devicetoken",
|
||||
index=models.Index(fields=["key"], name="authentik_e_key_504bbc_idx"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="deviceauthenticationtoken",
|
||||
name="connector",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="authentik_endpoints_connectors_agent.agentconnector",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="deviceauthenticationtoken",
|
||||
name="device",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to="authentik_endpoints.device"
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="deviceauthenticationtoken",
|
||||
name="device_token",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="authentik_endpoints_connectors_agent.devicetoken",
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="deviceauthenticationtoken",
|
||||
index=models.Index(fields=["expires"], name="authentik_e_expires_d52fb2_idx"),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="deviceauthenticationtoken",
|
||||
index=models.Index(fields=["expiring"], name="authentik_e_expirin_e9b873_idx"),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="deviceauthenticationtoken",
|
||||
index=models.Index(
|
||||
fields=["expiring", "expires"], name="authentik_e_expirin_8c95fe_idx"
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,70 @@
|
||||
# Generated by Django 5.2.8 on 2025-11-30 22:04
|
||||
|
||||
import authentik.lib.utils.time
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
(
|
||||
"authentik_endpoints_connectors_agent",
|
||||
"0002_deviceauthenticationtoken_alter_devicetoken_options_and_more",
|
||||
),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="agentconnector",
|
||||
name="auth_session_duration",
|
||||
field=models.TextField(
|
||||
default="hours=8", validators=[authentik.lib.utils.time.timedelta_string_validator]
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="deviceauthenticationtoken",
|
||||
name="user",
|
||||
field=models.ForeignKey(
|
||||
default=None,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="AppleNonce",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
|
||||
),
|
||||
),
|
||||
("expires", models.DateTimeField(default=None, null=True)),
|
||||
("expiring", models.BooleanField(default=True)),
|
||||
("nonce", models.TextField()),
|
||||
(
|
||||
"device_token",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="authentik_endpoints_connectors_agent.devicetoken",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Apple Nonce",
|
||||
"verbose_name_plural": "Apple Nonces",
|
||||
"abstract": False,
|
||||
"indexes": [
|
||||
models.Index(fields=["expires"], name="authentik_e_expires_e5d275_idx"),
|
||||
models.Index(fields=["expiring"], name="authentik_e_expirin_0b4d8e_idx"),
|
||||
models.Index(
|
||||
fields=["expiring", "expires"], name="authentik_e_expirin_355561_idx"
|
||||
),
|
||||
],
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -5,9 +5,15 @@ from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework.serializers import Serializer
|
||||
|
||||
from authentik.core.models import ExpiringModel, default_token_key
|
||||
from authentik.core.models import ExpiringModel, User, default_token_key
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.endpoints.models import Connector, DeviceConnection, DeviceGroup, DeviceUserBinding
|
||||
from authentik.endpoints.models import (
|
||||
Connector,
|
||||
Device,
|
||||
DeviceAccessGroup,
|
||||
DeviceConnection,
|
||||
DeviceUserBinding,
|
||||
)
|
||||
from authentik.flows.stage import StageView
|
||||
from authentik.lib.generators import generate_key
|
||||
from authentik.lib.models import SerializerModel
|
||||
@@ -20,17 +26,25 @@ if TYPE_CHECKING:
|
||||
class AgentConnector(Connector):
|
||||
"""Configure authentication and add device compliance using the authentik Agent."""
|
||||
|
||||
nss_uid_offset = models.PositiveIntegerField(default=1000)
|
||||
nss_gid_offset = models.PositiveIntegerField(default=1000)
|
||||
authentication_flow = models.ForeignKey(
|
||||
"authentik_flows.Flow", null=True, on_delete=models.SET_DEFAULT, default=None
|
||||
)
|
||||
auth_terminate_session_on_expiry = models.BooleanField(default=False)
|
||||
refresh_interval = models.TextField(
|
||||
default="minutes=30",
|
||||
validators=[timedelta_string_validator],
|
||||
)
|
||||
|
||||
auth_session_duration = models.TextField(
|
||||
default="hours=8", validators=[timedelta_string_validator]
|
||||
)
|
||||
auth_terminate_session_on_expiry = models.BooleanField(default=False)
|
||||
authorization_flow = models.ForeignKey(
|
||||
"authentik_flows.Flow", null=True, on_delete=models.SET_DEFAULT, default=None
|
||||
)
|
||||
jwt_federation_providers = models.ManyToManyField(
|
||||
"authentik_providers_oauth2.OAuth2Provider", blank=True, default=None
|
||||
)
|
||||
|
||||
nss_uid_offset = models.PositiveIntegerField(default=1000)
|
||||
nss_gid_offset = models.PositiveIntegerField(default=1000)
|
||||
|
||||
challenge_key = models.ForeignKey(CertificateKeyPair, on_delete=models.CASCADE, null=True)
|
||||
|
||||
@property
|
||||
@@ -66,11 +80,11 @@ class AgentConnector(Connector):
|
||||
|
||||
class AgentDeviceConnection(DeviceConnection):
|
||||
|
||||
apple_signing_key = models.TextField()
|
||||
apple_encryption_key = models.TextField()
|
||||
apple_key_exchange_key = models.TextField()
|
||||
apple_sign_key_id = models.TextField()
|
||||
apple_encryption_key = models.TextField()
|
||||
apple_enc_key_id = models.TextField()
|
||||
apple_signing_key = models.TextField()
|
||||
apple_sign_key_id = models.TextField()
|
||||
|
||||
|
||||
class AgentDeviceUserBinding(DeviceUserBinding):
|
||||
@@ -80,20 +94,30 @@ class AgentDeviceUserBinding(DeviceUserBinding):
|
||||
|
||||
|
||||
class DeviceToken(ExpiringModel):
|
||||
"""Per-device token used for authentication."""
|
||||
|
||||
token_uuid = models.UUIDField(primary_key=True, default=uuid4)
|
||||
device = models.ForeignKey(AgentDeviceConnection, on_delete=models.CASCADE)
|
||||
key = models.TextField(default=generate_key)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Device Token")
|
||||
verbose_name_plural = _("Device Tokens")
|
||||
indexes = ExpiringModel.Meta.indexes + [
|
||||
models.Index(fields=["key"]),
|
||||
]
|
||||
|
||||
|
||||
class EnrollmentToken(ExpiringModel, SerializerModel):
|
||||
"""Token used during enrollment, a device will receive
|
||||
a device token for further authentication"""
|
||||
|
||||
token_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4)
|
||||
name = models.TextField()
|
||||
key = models.TextField(default=default_token_key)
|
||||
connector = models.ForeignKey(AgentConnector, on_delete=models.CASCADE)
|
||||
device_group = models.ForeignKey(
|
||||
DeviceGroup, on_delete=models.SET_DEFAULT, default=None, null=True
|
||||
DeviceAccessGroup, on_delete=models.SET_DEFAULT, default=None, null=True
|
||||
)
|
||||
|
||||
@property
|
||||
@@ -113,3 +137,29 @@ class EnrollmentToken(ExpiringModel, SerializerModel):
|
||||
permissions = [
|
||||
("view_enrollment_token_key", _("View token's key")),
|
||||
]
|
||||
|
||||
|
||||
class DeviceAuthenticationToken(ExpiringModel):
|
||||
|
||||
identifier = models.UUIDField(default=uuid4, primary_key=True)
|
||||
device = models.ForeignKey(Device, on_delete=models.CASCADE)
|
||||
device_token = models.ForeignKey(DeviceToken, on_delete=models.CASCADE)
|
||||
connector = models.ForeignKey(AgentConnector, on_delete=models.CASCADE)
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE, null=True, default=None)
|
||||
token = models.TextField()
|
||||
|
||||
def __str__(self):
|
||||
return f"Device authentication token {self.identifier}"
|
||||
|
||||
class Meta(ExpiringModel.Meta):
|
||||
verbose_name = _("Device authentication token")
|
||||
verbose_name_plural = _("Device authentication tokens")
|
||||
|
||||
|
||||
class AppleNonce(ExpiringModel):
|
||||
nonce = models.TextField()
|
||||
device_token = models.ForeignKey(DeviceToken, on_delete=models.CASCADE)
|
||||
|
||||
class Meta(ExpiringModel.Meta):
|
||||
verbose_name = _("Apple Nonce")
|
||||
verbose_name_plural = _("Apple Nonces")
|
||||
|
||||
@@ -4,11 +4,12 @@ from django.urls import reverse
|
||||
from django.utils.timezone import now
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from authentik.blueprints.tests import reconcile_app
|
||||
from authentik.core.tests.utils import create_test_admin_user
|
||||
from authentik.endpoints.connectors.agent.api.connectors import AgentDeviceConnection
|
||||
from authentik.endpoints.connectors.agent.models import AgentConnector, DeviceToken, EnrollmentToken
|
||||
from authentik.endpoints.facts import OSFamily
|
||||
from authentik.endpoints.models import Device, DeviceGroup
|
||||
from authentik.endpoints.models import Device, DeviceAccessGroup
|
||||
from authentik.lib.generators import generate_id
|
||||
|
||||
CHECK_IN_DATA_VALID = {
|
||||
@@ -35,9 +36,7 @@ CHECK_IN_DATA_VALID = {
|
||||
class TestAgentAPI(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.connector = AgentConnector.objects.create(
|
||||
name=generate_id(),
|
||||
)
|
||||
self.connector = AgentConnector.objects.create(name=generate_id())
|
||||
self.token = EnrollmentToken.objects.create(name=generate_id(), connector=self.connector)
|
||||
self.device = Device.objects.create(
|
||||
identifier=generate_id(),
|
||||
@@ -60,7 +59,7 @@ class TestAgentAPI(APITestCase):
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_enroll_group(self):
|
||||
device_group = DeviceGroup.objects.create(name=generate_id())
|
||||
device_group = DeviceAccessGroup.objects.create(name=generate_id())
|
||||
self.token.device_group = device_group
|
||||
self.token.save()
|
||||
ident = generate_id()
|
||||
@@ -72,7 +71,7 @@ class TestAgentAPI(APITestCase):
|
||||
self.assertEqual(response.status_code, 200)
|
||||
device = Device.objects.filter(identifier=ident).first()
|
||||
self.assertIsNotNone(device)
|
||||
self.assertEqual(device.group, device_group)
|
||||
self.assertEqual(device.access_group, device_group)
|
||||
|
||||
def test_enroll_expired(self):
|
||||
dev_id = generate_id()
|
||||
@@ -87,10 +86,11 @@ class TestAgentAPI(APITestCase):
|
||||
self.assertEqual(response.status_code, 403)
|
||||
self.assertFalse(Device.objects.filter(identifier=dev_id).exists())
|
||||
|
||||
@reconcile_app("authentik_crypto")
|
||||
def test_config(self):
|
||||
response = self.client.get(
|
||||
reverse("authentik_api:agentconnector-agent-config"),
|
||||
HTTP_AUTHORIZATION=f"Bearer {self.device_token.key}",
|
||||
HTTP_AUTHORIZATION=f"Bearer+agent {self.device_token.key}",
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
@@ -98,7 +98,7 @@ class TestAgentAPI(APITestCase):
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:agentconnector-check-in"),
|
||||
data=CHECK_IN_DATA_VALID,
|
||||
HTTP_AUTHORIZATION=f"Bearer {self.device_token.key}",
|
||||
HTTP_AUTHORIZATION=f"Bearer+agent {self.device_token.key}",
|
||||
)
|
||||
self.assertEqual(response.status_code, 204)
|
||||
|
||||
@@ -109,7 +109,7 @@ class TestAgentAPI(APITestCase):
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:agentconnector-check-in"),
|
||||
data=CHECK_IN_DATA_VALID,
|
||||
HTTP_AUTHORIZATION=f"Bearer {self.device_token.key}",
|
||||
HTTP_AUTHORIZATION=f"Bearer+agent {self.device_token.key}",
|
||||
)
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
@@ -120,7 +120,7 @@ class TestAgentAPI(APITestCase):
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:agentconnector-check-in"),
|
||||
data=CHECK_IN_DATA_VALID,
|
||||
HTTP_AUTHORIZATION=f"Bearer {self.device_token.key}",
|
||||
HTTP_AUTHORIZATION=f"Bearer+agent {self.device_token.key}",
|
||||
)
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
|
||||
@@ -67,9 +67,9 @@ class NetworkSerializer(Serializer):
|
||||
|
||||
|
||||
class HardwareSerializer(Serializer):
|
||||
model = CharField()
|
||||
manufacturer = CharField()
|
||||
serial = CharField(allow_blank=True)
|
||||
model = CharField(required=False)
|
||||
manufacturer = CharField(required=False)
|
||||
serial = CharField()
|
||||
|
||||
cpu_name = CharField(required=False)
|
||||
cpu_count = IntegerField(required=False)
|
||||
@@ -91,6 +91,18 @@ class ProcessSerializer(Serializer):
|
||||
user = CharField(required=False)
|
||||
|
||||
|
||||
class DeviceUserSerializer(Serializer):
|
||||
id = CharField(required=True)
|
||||
username = CharField(required=False)
|
||||
name = CharField(required=False)
|
||||
home = CharField(required=False)
|
||||
|
||||
|
||||
class DeviceGroupSerializer(Serializer):
|
||||
id = CharField(required=True)
|
||||
name = CharField(required=False)
|
||||
|
||||
|
||||
class DeviceFacts(Serializer):
|
||||
os = OperatingSystemSerializer(required=False, allow_null=True)
|
||||
disks = ListField(child=DiskSerializer(), required=False, allow_null=True)
|
||||
@@ -98,4 +110,6 @@ class DeviceFacts(Serializer):
|
||||
hardware = HardwareSerializer(required=False, allow_null=True)
|
||||
software = ListField(child=SoftwareSerializer(), required=False, allow_null=True)
|
||||
processes = ListField(child=ProcessSerializer(), required=False, allow_null=True)
|
||||
users = ListField(child=DeviceUserSerializer(), required=False, allow_null=True)
|
||||
groups = ListField(child=DeviceGroupSerializer(), required=False, allow_null=True)
|
||||
vendor = JSONDictField(required=False)
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
# Generated by Django 5.2.8 on 2025-11-27 00:16
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_endpoints", "0001_initial"),
|
||||
("authentik_endpoints_connectors_agent", "0001_initial"),
|
||||
("authentik_policies", "0011_policybinding_failure_result_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameModel(
|
||||
old_name="DeviceGroup",
|
||||
new_name="DeviceAccessGroup",
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name="device",
|
||||
options={"verbose_name": "Device", "verbose_name_plural": "Devices"},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name="deviceaccessgroup",
|
||||
options={
|
||||
"verbose_name": "Device access group",
|
||||
"verbose_name_plural": "Device access groups",
|
||||
},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name="deviceconnection",
|
||||
options={
|
||||
"verbose_name": "Device connection",
|
||||
"verbose_name_plural": "Device connections",
|
||||
},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name="devicefactsnapshot",
|
||||
options={
|
||||
"verbose_name": "Device fact snapshot",
|
||||
"verbose_name_plural": "Device fact snapshots",
|
||||
},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name="deviceuserbinding",
|
||||
options={
|
||||
"verbose_name": "Device User binding",
|
||||
"verbose_name_plural": "Device User bindings",
|
||||
},
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name="device",
|
||||
old_name="group",
|
||||
new_name="access_group",
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="device",
|
||||
name="name",
|
||||
field=models.TextField(unique=True),
|
||||
),
|
||||
]
|
||||
@@ -6,6 +6,7 @@ from django.core.cache import cache
|
||||
from django.db import models
|
||||
from django.db.models import OuterRef, Subquery
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from model_utils.managers import InheritanceManager
|
||||
from rest_framework.serializers import Serializer
|
||||
from structlog.stdlib import get_logger
|
||||
@@ -30,10 +31,12 @@ DEVICE_FACTS_CACHE_TIMEOUT = 3600
|
||||
class Device(ExpiringModel, AttributesMixin, PolicyBindingModel):
|
||||
device_uuid = models.UUIDField(default=uuid4, primary_key=True)
|
||||
|
||||
name = models.TextField()
|
||||
name = models.TextField(unique=True)
|
||||
identifier = models.TextField(unique=True)
|
||||
connections = models.ManyToManyField("Connector", through="DeviceConnection")
|
||||
group = models.ForeignKey("DeviceGroup", null=True, on_delete=models.SET_DEFAULT, default=None)
|
||||
access_group = models.ForeignKey(
|
||||
"DeviceAccessGroup", null=True, on_delete=models.SET_DEFAULT, default=None
|
||||
)
|
||||
|
||||
@property
|
||||
def cache_key_facts(self):
|
||||
@@ -64,6 +67,13 @@ class Device(ExpiringModel, AttributesMixin, PolicyBindingModel):
|
||||
last_updated = max(last_updated, snapshort_created)
|
||||
return DeviceFactSnapshot(data=data, created=last_updated)
|
||||
|
||||
def __str__(self):
|
||||
return f"Device {self.name} {self.identifier} ({self.pk})"
|
||||
|
||||
class Meta(ExpiringModel.Meta):
|
||||
verbose_name = _("Device")
|
||||
verbose_name_plural = _("Devices")
|
||||
|
||||
|
||||
class DeviceUserBinding(PolicyBinding):
|
||||
is_primary = models.BooleanField(default=False)
|
||||
@@ -71,6 +81,10 @@ class DeviceUserBinding(PolicyBinding):
|
||||
# by a connector and not manually
|
||||
connector = models.ForeignKey("Connector", on_delete=models.CASCADE, null=True)
|
||||
|
||||
class Meta(PolicyBinding.Meta):
|
||||
verbose_name = _("Device User binding")
|
||||
verbose_name_plural = _("Device User bindings")
|
||||
|
||||
|
||||
class DeviceConnection(SerializerModel):
|
||||
device_connection_uuid = models.UUIDField(default=uuid4, primary_key=True)
|
||||
@@ -96,6 +110,10 @@ class DeviceConnection(SerializerModel):
|
||||
|
||||
return DeviceConnectionSerializer
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Device connection")
|
||||
verbose_name_plural = _("Device connections")
|
||||
|
||||
|
||||
class DeviceFactSnapshot(ExpiringModel, SerializerModel):
|
||||
snapshot_id = models.UUIDField(primary_key=True, default=uuid4)
|
||||
@@ -112,19 +130,24 @@ class DeviceFactSnapshot(ExpiringModel, SerializerModel):
|
||||
|
||||
return DeviceFactSnapshotSerializer
|
||||
|
||||
class Meta(ExpiringModel.Meta):
|
||||
verbose_name = _("Device fact snapshot")
|
||||
verbose_name_plural = _("Device fact snapshots")
|
||||
|
||||
|
||||
class Connector(ScheduledModel, SerializerModel):
|
||||
connector_uuid = models.UUIDField(default=uuid4, primary_key=True)
|
||||
|
||||
name = models.TextField()
|
||||
enabled = models.BooleanField(default=True)
|
||||
objects = InheritanceManager()
|
||||
|
||||
snapshot_expiry = models.TextField(
|
||||
default="hours=24",
|
||||
validators=[timedelta_string_validator],
|
||||
)
|
||||
|
||||
objects = InheritanceManager()
|
||||
|
||||
@property
|
||||
def stage(self) -> type[StageView] | None:
|
||||
return None
|
||||
@@ -152,10 +175,20 @@ class Connector(ScheduledModel, SerializerModel):
|
||||
]
|
||||
|
||||
|
||||
class DeviceGroup(PolicyBindingModel):
|
||||
class DeviceAccessGroup(PolicyBindingModel):
|
||||
|
||||
name = models.TextField(unique=True)
|
||||
|
||||
@property
|
||||
def serializer(self) -> type[Serializer]:
|
||||
from authentik.endpoints.api.device_access_group import DeviceAccessGroupSerializer
|
||||
|
||||
return DeviceAccessGroupSerializer
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Device access group")
|
||||
verbose_name_plural = _("Device access groups")
|
||||
|
||||
|
||||
class EndpointStage(Stage):
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from authentik.endpoints.api.connectors import ConnectorViewSet
|
||||
from authentik.endpoints.api.device_group import DeviceGroupViewSet
|
||||
from authentik.endpoints.api.device_access_group import DeviceAccessGroupViewSet
|
||||
from authentik.endpoints.api.device_user_bindings import DeviceUserBindingViewSet
|
||||
from authentik.endpoints.api.devices import DeviceViewSet
|
||||
|
||||
@@ -7,5 +7,5 @@ api_urlpatterns = [
|
||||
("endpoints/connectors", ConnectorViewSet, "endpoint_connectors"),
|
||||
("endpoints/devices", DeviceViewSet, "endpoint_device"),
|
||||
("endpoints/device_bindings", DeviceUserBindingViewSet, "endpoint_device_bindings"),
|
||||
("endpoints/device_groups", DeviceGroupViewSet, "endpoint_device_groups"),
|
||||
("endpoints/device_access_groups", DeviceAccessGroupViewSet, "endpoint_device_access_groups"),
|
||||
]
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
from django.http import Http404, HttpResponseBadRequest
|
||||
from django.urls import reverse
|
||||
from django.utils.timezone import now
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema
|
||||
from rest_framework.authentication import get_authorization_header
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.api.authentication import validate_auth
|
||||
from authentik.endpoints.connectors.agent.api.agent import (
|
||||
AgentAuthenticationResponse,
|
||||
AgentTokenResponseSerializer,
|
||||
)
|
||||
from authentik.endpoints.connectors.agent.auth import AgentAuth
|
||||
from authentik.endpoints.connectors.agent.models import (
|
||||
DeviceAuthenticationToken,
|
||||
DeviceToken,
|
||||
)
|
||||
from authentik.endpoints.models import Device
|
||||
from authentik.enterprise.endpoints.connectors.agent.auth import (
|
||||
agent_auth_fed_validate,
|
||||
agent_auth_issue_token,
|
||||
check_device_policies,
|
||||
)
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.flows.planner import PLAN_CONTEXT_DEVICE
|
||||
from authentik.stages.password.stage import PLAN_CONTEXT_METHOD, PLAN_CONTEXT_METHOD_ARGS
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
class AgentConnectorViewSetMixin:
|
||||
|
||||
@extend_schema(
|
||||
request=OpenApiTypes.NONE,
|
||||
responses=AgentAuthenticationResponse(),
|
||||
)
|
||||
@action(methods=["POST"], detail=False, authentication_classes=[AgentAuth])
|
||||
def auth_ia(self, request: Request) -> Response:
|
||||
token: DeviceToken = request.auth
|
||||
auth_token = DeviceAuthenticationToken.objects.create(
|
||||
device=token.device.device,
|
||||
device_token=token,
|
||||
connector=token.device.connector.agentconnector,
|
||||
)
|
||||
return Response(
|
||||
{
|
||||
"url": request.build_absolute_uri(
|
||||
reverse(
|
||||
"authentik_enterprise_endpoints_connectors_agent:authenticate",
|
||||
kwargs={"token_uuid": auth_token.identifier},
|
||||
)
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
@extend_schema(
|
||||
request=OpenApiTypes.NONE,
|
||||
parameters=[OpenApiParameter("device", OpenApiTypes.STR, location="query", required=True)],
|
||||
responses={
|
||||
200: AgentTokenResponseSerializer(),
|
||||
404: OpenApiResponse(description="Device not found"),
|
||||
},
|
||||
)
|
||||
@action(
|
||||
methods=["POST"],
|
||||
detail=False,
|
||||
pagination_class=None,
|
||||
filter_backends=[],
|
||||
permission_classes=[],
|
||||
authentication_classes=[],
|
||||
)
|
||||
def auth_fed(self, request: Request) -> Response:
|
||||
raw_token = validate_auth(get_authorization_header(request))
|
||||
if not raw_token:
|
||||
LOGGER.warning("Missing token")
|
||||
return HttpResponseBadRequest()
|
||||
device = Device.objects.filter(name=request.query_params.get("device")).first()
|
||||
if not device:
|
||||
LOGGER.warning("Couldn't find device")
|
||||
raise Http404
|
||||
|
||||
federated_token, connector = agent_auth_fed_validate(raw_token, device)
|
||||
LOGGER.info(
|
||||
"successfully verified JWT with provider", provider=federated_token.provider.name
|
||||
)
|
||||
|
||||
policy_result = check_device_policies(device, federated_token.user, request._request)
|
||||
if not policy_result.passing:
|
||||
raise ValidationError(
|
||||
{"policy_result": "Policy denied access", "policy_messages": policy_result.messages}
|
||||
)
|
||||
|
||||
token, exp = agent_auth_issue_token(device, connector, federated_token.user)
|
||||
rel_exp = int((exp - now()).total_seconds())
|
||||
Event.new(
|
||||
EventAction.LOGIN,
|
||||
**{
|
||||
PLAN_CONTEXT_METHOD: "jwt",
|
||||
PLAN_CONTEXT_METHOD_ARGS: {
|
||||
"jwt": federated_token,
|
||||
"provider": federated_token.provider,
|
||||
},
|
||||
PLAN_CONTEXT_DEVICE: device,
|
||||
},
|
||||
).from_http(request, user=federated_token.user)
|
||||
return Response({"token": token, "expires_in": rel_exp})
|
||||
@@ -9,4 +9,5 @@ class AuthentikEnterpriseEndpointsConnectorAgentAppConfig(EnterpriseConfig):
|
||||
default = True
|
||||
mountpoints = {
|
||||
"authentik.enterprise.endpoints.connectors.agent.urls_root": "",
|
||||
"authentik.enterprise.endpoints.connectors.agent.urls": "endpoints/agent/",
|
||||
}
|
||||
|
||||
87
authentik/enterprise/endpoints/connectors/agent/auth.py
Normal file
87
authentik/enterprise/endpoints/connectors/agent/auth.py
Normal file
@@ -0,0 +1,87 @@
|
||||
from django.http import Http404, HttpRequest
|
||||
from django.utils.timezone import now
|
||||
from jwt import PyJWTError, decode, encode
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.core.models import User
|
||||
from authentik.crypto.apps import MANAGED_KEY
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.endpoints.connectors.agent.models import AgentConnector
|
||||
from authentik.endpoints.models import Device
|
||||
from authentik.lib.utils.time import timedelta_from_string
|
||||
from authentik.policies.engine import PolicyEngine
|
||||
from authentik.policies.models import PolicyBindingModel
|
||||
from authentik.providers.oauth2.models import AccessToken, JWTAlgorithms, OAuth2Provider
|
||||
|
||||
LOGGER = get_logger()
|
||||
PLATFORM_ISSUER = "goauthentik.io/platform"
|
||||
|
||||
|
||||
def agent_auth_issue_token(device: Device, connector: AgentConnector, user: User, **kwargs):
|
||||
kp = CertificateKeyPair.objects.filter(managed=MANAGED_KEY).first()
|
||||
if not kp:
|
||||
return None, None
|
||||
exp = now() + timedelta_from_string(connector.auth_session_duration)
|
||||
token = encode(
|
||||
{
|
||||
"iss": PLATFORM_ISSUER,
|
||||
"aud": str(device.pk),
|
||||
"iat": int(now().timestamp()),
|
||||
"exp": int(exp.timestamp()),
|
||||
"preferred_username": user.username,
|
||||
**kwargs,
|
||||
},
|
||||
kp.private_key,
|
||||
headers={
|
||||
"kid": kp.kid,
|
||||
},
|
||||
algorithm=JWTAlgorithms.from_private_key(kp.private_key),
|
||||
)
|
||||
return token, exp
|
||||
|
||||
|
||||
def agent_auth_fed_validate(
|
||||
raw_token: str, device: Device
|
||||
) -> tuple[AccessToken, AgentConnector | None]:
|
||||
connectors_for_device = AgentConnector.objects.filter(device__in=[device])
|
||||
connector = connectors_for_device.first()
|
||||
providers = OAuth2Provider.objects.filter(agentconnector__in=connectors_for_device)
|
||||
federated_token = AccessToken.objects.filter(token=raw_token, provider__in=providers).first()
|
||||
if not federated_token:
|
||||
LOGGER.warning("Couldn't lookup provider")
|
||||
raise Http404
|
||||
_key, _alg = federated_token.provider.jwt_key
|
||||
try:
|
||||
decode(
|
||||
raw_token,
|
||||
_key,
|
||||
algorithms=[_alg],
|
||||
options={
|
||||
"verify_aud": False,
|
||||
},
|
||||
)
|
||||
return federated_token, connector
|
||||
except (PyJWTError, ValueError, TypeError, AttributeError) as exc:
|
||||
LOGGER.warning("failed to verify JWT", exc=exc, provider=federated_token.provider.name)
|
||||
raise ValidationError() from None
|
||||
|
||||
|
||||
def check_device_policies(device: Device, user: User, request: HttpRequest):
|
||||
"""Check policies bound to device group and device"""
|
||||
if device.access_group:
|
||||
result = check_pbm_policies(device.access_group, user, request)
|
||||
if result.passing:
|
||||
return result
|
||||
return check_pbm_policies(device, user, request)
|
||||
|
||||
|
||||
def check_pbm_policies(pbm: PolicyBindingModel, user: User, request: HttpRequest):
|
||||
policy_engine = PolicyEngine(pbm, user, request)
|
||||
policy_engine.use_cache = False
|
||||
policy_engine.empty_result = False
|
||||
policy_engine.mode = pbm.policy_engine_mode
|
||||
policy_engine.build()
|
||||
result = policy_engine.result
|
||||
LOGGER.debug("PolicyAccessView user_has_access", user=user.username, result=result, pbm=pbm.pk)
|
||||
return result
|
||||
121
authentik/enterprise/endpoints/connectors/agent/http.py
Normal file
121
authentik/enterprise/endpoints/connectors/agent/http.py
Normal file
@@ -0,0 +1,121 @@
|
||||
from base64 import urlsafe_b64encode
|
||||
from json import dumps
|
||||
from secrets import token_bytes
|
||||
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives import hashes, serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import ec
|
||||
from cryptography.hazmat.primitives.ciphers import Cipher
|
||||
from cryptography.hazmat.primitives.ciphers.algorithms import AES
|
||||
from cryptography.hazmat.primitives.ciphers.modes import GCM
|
||||
from cryptography.hazmat.primitives.kdf.concatkdf import ConcatKDFHash
|
||||
from django.http import HttpResponse
|
||||
from jwcrypto.common import base64url_decode, base64url_encode
|
||||
|
||||
from authentik.endpoints.connectors.agent.models import AgentDeviceConnection
|
||||
|
||||
|
||||
def length_prefixed(data: bytes) -> bytes:
|
||||
length = len(data)
|
||||
return length.to_bytes(4, "big") + data
|
||||
|
||||
|
||||
def build_apu(public_key: ec.EllipticCurvePublicKey):
|
||||
# X9.63 representation: 0x04 || X || Y
|
||||
public_numbers = public_key.public_numbers()
|
||||
|
||||
x_bytes = public_numbers.x.to_bytes(32, "big")
|
||||
y_bytes = public_numbers.y.to_bytes(32, "big")
|
||||
|
||||
x963 = bytes([0x04]) + x_bytes + y_bytes
|
||||
|
||||
result = length_prefixed(b"APPLE") + length_prefixed(x963)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def encrypt_token_with_a256_gcm(body: dict, device_encryption_key: str, apv: bytes) -> str:
|
||||
ephemeral_key = ec.generate_private_key(curve=ec.SECP256R1())
|
||||
device_public_key = serialization.load_pem_public_key(
|
||||
device_encryption_key.encode(), backend=default_backend()
|
||||
)
|
||||
|
||||
shared_secret_z = ephemeral_key.exchange(ec.ECDH(), device_public_key)
|
||||
|
||||
apu = build_apu(ephemeral_key.public_key())
|
||||
|
||||
jwe_header = {
|
||||
"enc": "A256GCM",
|
||||
"kid": "ephemeralKey",
|
||||
"epk": {
|
||||
"x": base64url_encode(
|
||||
ephemeral_key.public_key().public_numbers().x.to_bytes(32, "big")
|
||||
),
|
||||
"y": base64url_encode(
|
||||
ephemeral_key.public_key().public_numbers().y.to_bytes(32, "big")
|
||||
),
|
||||
"kty": "EC",
|
||||
"crv": "P-256",
|
||||
},
|
||||
"typ": "platformsso-login-response+jwt",
|
||||
"alg": "ECDH-ES",
|
||||
"apu": base64url_encode(apu),
|
||||
"apv": base64url_encode(apv),
|
||||
}
|
||||
|
||||
party_u_info = length_prefixed(apu)
|
||||
party_v_info = length_prefixed(apv)
|
||||
supp_pub_info = (256).to_bytes(4, "big")
|
||||
|
||||
other_info = length_prefixed(b"A256GCM") + party_u_info + party_v_info + supp_pub_info
|
||||
|
||||
ckdf = ConcatKDFHash(
|
||||
algorithm=hashes.SHA256(),
|
||||
length=32,
|
||||
otherinfo=other_info,
|
||||
)
|
||||
|
||||
derived_key = ckdf.derive(shared_secret_z)
|
||||
|
||||
nonce = token_bytes(96)
|
||||
|
||||
header_json = dumps(jwe_header, separators=(",", ":")).encode()
|
||||
aad = urlsafe_b64encode(header_json).rstrip(b"=")
|
||||
|
||||
cipher = Cipher(AES(derived_key), GCM(nonce))
|
||||
encryptor = cipher.encryptor()
|
||||
encryptor.authenticate_additional_data(aad)
|
||||
ciphertext = encryptor.update(dumps(body).encode()) + encryptor.finalize()
|
||||
|
||||
# base64url encoding
|
||||
protected_b64 = urlsafe_b64encode(header_json).rstrip(b"=")
|
||||
iv_b64 = urlsafe_b64encode(nonce).rstrip(b"=")
|
||||
ciphertext_b64 = urlsafe_b64encode(ciphertext).rstrip(b"=")
|
||||
tag_b64 = urlsafe_b64encode(encryptor.tag).rstrip(b"=")
|
||||
|
||||
jwe_compact = b".".join(
|
||||
[
|
||||
protected_b64,
|
||||
b"",
|
||||
iv_b64,
|
||||
ciphertext_b64,
|
||||
tag_b64,
|
||||
]
|
||||
)
|
||||
return jwe_compact.decode()
|
||||
|
||||
|
||||
class JWEResponse(HttpResponse):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
data: dict,
|
||||
device: AgentDeviceConnection,
|
||||
apv: str,
|
||||
):
|
||||
super().__init__(
|
||||
content=encrypt_token_with_a256_gcm(
|
||||
data, device.apple_encryption_key, base64url_decode(apv)
|
||||
),
|
||||
content_type="application/platformsso-login-response+jwt",
|
||||
)
|
||||
@@ -0,0 +1,55 @@
|
||||
from json import loads
|
||||
from urllib.parse import quote
|
||||
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from authentik.blueprints.tests import reconcile_app
|
||||
from authentik.core.tests.utils import create_test_user
|
||||
from authentik.endpoints.connectors.agent.models import (
|
||||
AgentConnector,
|
||||
AgentDeviceConnection,
|
||||
AppleNonce,
|
||||
DeviceToken,
|
||||
EnrollmentToken,
|
||||
)
|
||||
from authentik.endpoints.models import Device
|
||||
from authentik.lib.generators import generate_id
|
||||
|
||||
|
||||
class TestAppleViews(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.connector = AgentConnector.objects.create(name=generate_id())
|
||||
self.token = EnrollmentToken.objects.create(name=generate_id(), connector=self.connector)
|
||||
self.device = Device.objects.create(
|
||||
name=generate_id(),
|
||||
identifier=generate_id(),
|
||||
)
|
||||
self.connection = AgentDeviceConnection.objects.create(
|
||||
device=self.device,
|
||||
connector=self.connector,
|
||||
)
|
||||
self.user = create_test_user()
|
||||
|
||||
def test_apple_site_association(self):
|
||||
res = self.client.get(reverse("authentik_enterprise_endpoints_connectors_agent_root:asa"))
|
||||
self.assertEqual(res.status_code, 200)
|
||||
|
||||
@reconcile_app("authentik_crypto")
|
||||
def test_apple_jwks(self):
|
||||
res = self.client.get(reverse("authentik_enterprise_endpoints_connectors_agent:psso-jwks"))
|
||||
self.assertEqual(res.status_code, 200)
|
||||
|
||||
def test_apple_nonce(self):
|
||||
device_token = DeviceToken.objects.create(device=self.connection)
|
||||
res = self.client.post(
|
||||
reverse("authentik_enterprise_endpoints_connectors_agent:psso-nonce"),
|
||||
data={"x-ak-device-token": quote(device_token.key)},
|
||||
)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
nonce = loads(res.content.decode()).get("Nonce")
|
||||
self.assertIsNotNone(nonce)
|
||||
db_nonce = AppleNonce.objects.filter(nonce=nonce).first()
|
||||
self.assertIsNotNone(db_nonce)
|
||||
self.assertFalse(db_nonce.is_expired)
|
||||
@@ -0,0 +1,39 @@
|
||||
from base64 import urlsafe_b64decode
|
||||
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import ec
|
||||
from django.test import TestCase
|
||||
from jwcrypto.jwe import JWE
|
||||
from jwcrypto.jwk import JWK
|
||||
|
||||
from authentik.enterprise.endpoints.connectors.agent.http import (
|
||||
base64url_decode,
|
||||
encrypt_token_with_a256_gcm,
|
||||
)
|
||||
|
||||
|
||||
class TestAppleJWE(TestCase):
|
||||
|
||||
def test_encrypt(self):
|
||||
data = {"foo": "bar"}
|
||||
apv = (
|
||||
"AAAABUFwcGxlAAAAQQTFgZOospN6KbkhXhx1lfa-AKYxjEfJhTJrkpdEY_srMmkPzS7VN0Bzt2AtNBEXE"
|
||||
"aphDONiP2Mq6Oxytv5JKOxHAAAAJDgyOThERkY5LTVFMUUtNEUwMS04OEUwLUI3QkQzOUM4QjA3Qw"
|
||||
)
|
||||
key = ec.generate_private_key(curve=ec.SECP256R1())
|
||||
pub = (
|
||||
key.public_key()
|
||||
.public_bytes(
|
||||
encoding=serialization.Encoding.PEM,
|
||||
format=serialization.PublicFormat.SubjectPublicKeyInfo,
|
||||
)
|
||||
.decode()
|
||||
)
|
||||
res = encrypt_token_with_a256_gcm(data, pub, base64url_decode(apv))
|
||||
parsed = JWE()
|
||||
parsed.deserialize(res, JWK.from_pyca(key))
|
||||
payload = parsed.payload
|
||||
self.assertEqual(payload, b'{"foo": "bar"}')
|
||||
self.assertEqual(parsed.jose_header["apv"], apv)
|
||||
self.assertEqual(parsed.jose_header["typ"], "platformsso-login-response+jwt")
|
||||
self.assertIn(b"APPLE", urlsafe_b64decode(parsed.jose_header["apu"]))
|
||||
@@ -0,0 +1,110 @@
|
||||
from json import loads
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from django.urls import reverse
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from authentik.blueprints.tests import reconcile_app
|
||||
from authentik.core.tests.utils import create_test_user
|
||||
from authentik.endpoints.connectors.agent.models import (
|
||||
AgentConnector,
|
||||
AgentDeviceConnection,
|
||||
DeviceAuthenticationToken,
|
||||
DeviceToken,
|
||||
EnrollmentToken,
|
||||
)
|
||||
from authentik.endpoints.models import Device
|
||||
from authentik.enterprise.license import LicenseKey
|
||||
from authentik.enterprise.models import License
|
||||
from authentik.enterprise.tests.test_license import expiry_valid
|
||||
from authentik.lib.generators import generate_id
|
||||
|
||||
|
||||
class TestAppleRegister(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.connector = AgentConnector.objects.create(name=generate_id())
|
||||
self.token = EnrollmentToken.objects.create(name=generate_id(), connector=self.connector)
|
||||
self.device = Device.objects.create(
|
||||
name=generate_id(),
|
||||
identifier=generate_id(),
|
||||
)
|
||||
self.connection = AgentDeviceConnection.objects.create(
|
||||
device=self.device,
|
||||
connector=self.connector,
|
||||
)
|
||||
self.user = create_test_user()
|
||||
self.device_token = DeviceToken.objects.create(device=self.connection)
|
||||
|
||||
@patch(
|
||||
"authentik.enterprise.license.LicenseKey.validate",
|
||||
MagicMock(
|
||||
return_value=LicenseKey(
|
||||
aud="",
|
||||
exp=expiry_valid,
|
||||
name=generate_id(),
|
||||
internal_users=100,
|
||||
external_users=100,
|
||||
)
|
||||
),
|
||||
)
|
||||
@reconcile_app("authentik_crypto")
|
||||
def test_register_device(self):
|
||||
License.objects.create(key=generate_id())
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:psso-register-device"),
|
||||
data={
|
||||
"device_signing_key": generate_id(),
|
||||
"device_encryption_key": generate_id(),
|
||||
"sign_key_id": generate_id(),
|
||||
"enc_key_id": generate_id(),
|
||||
},
|
||||
HTTP_AUTHORIZATION=f"Bearer+agent {self.device_token.key}",
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertJSONEqual(
|
||||
response.content,
|
||||
{
|
||||
"client_id": str(self.connector.pk),
|
||||
"audience": str(self.device.pk),
|
||||
"issuer": "http://testserver/endpoints/agent/psso/token/",
|
||||
"jwks_endpoint": "http://testserver/endpoints/agent/psso/jwks/",
|
||||
"nonce_endpoint": "http://testserver/endpoints/agent/psso/nonce/",
|
||||
"token_endpoint": "http://testserver/endpoints/agent/psso/token/",
|
||||
},
|
||||
)
|
||||
|
||||
@patch(
|
||||
"authentik.enterprise.license.LicenseKey.validate",
|
||||
MagicMock(
|
||||
return_value=LicenseKey(
|
||||
aud="",
|
||||
exp=expiry_valid,
|
||||
name=generate_id(),
|
||||
internal_users=100,
|
||||
external_users=100,
|
||||
)
|
||||
),
|
||||
)
|
||||
@reconcile_app("authentik_crypto")
|
||||
def test_register_user(self):
|
||||
License.objects.create(key=generate_id())
|
||||
device_auth = DeviceAuthenticationToken.objects.create(
|
||||
device=self.device,
|
||||
device_token=self.device_token,
|
||||
connector=self.connector,
|
||||
user=self.user,
|
||||
token=generate_id(),
|
||||
)
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:psso-register-user"),
|
||||
data={
|
||||
"user_auth": device_auth.token,
|
||||
"user_secure_enclave_key": generate_id(),
|
||||
"enclave_key_id": generate_id(),
|
||||
},
|
||||
HTTP_AUTHORIZATION=f"Bearer+agent {self.device_token.key}",
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
body = loads(response.content)
|
||||
self.assertEqual(body["username"], self.user.username)
|
||||
@@ -0,0 +1,108 @@
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import ec
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
from jwt import encode
|
||||
|
||||
from authentik.blueprints.tests import reconcile_app
|
||||
from authentik.core.tests.utils import create_test_cert, create_test_user
|
||||
from authentik.crypto.builder import PrivateKeyAlg
|
||||
from authentik.endpoints.connectors.agent.models import (
|
||||
AgentConnector,
|
||||
AgentDeviceConnection,
|
||||
AgentDeviceUserBinding,
|
||||
AppleNonce,
|
||||
DeviceToken,
|
||||
EnrollmentToken,
|
||||
)
|
||||
from authentik.endpoints.models import Device
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.providers.oauth2.models import JWTAlgorithms
|
||||
|
||||
|
||||
class TestAppleToken(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.apple_sign_key = create_test_cert(PrivateKeyAlg.ECDSA)
|
||||
sign_key_pem = self.apple_sign_key.public_key.public_bytes(
|
||||
encoding=serialization.Encoding.PEM,
|
||||
format=serialization.PublicFormat.SubjectPublicKeyInfo,
|
||||
).decode()
|
||||
|
||||
self.enc_key = ec.generate_private_key(curve=ec.SECP256R1())
|
||||
self.enc_pub = (
|
||||
self.enc_key.public_key()
|
||||
.public_bytes(
|
||||
encoding=serialization.Encoding.PEM,
|
||||
format=serialization.PublicFormat.SubjectPublicKeyInfo,
|
||||
)
|
||||
.decode()
|
||||
)
|
||||
|
||||
self.connector = AgentConnector.objects.create(name=generate_id())
|
||||
self.token = EnrollmentToken.objects.create(name=generate_id(), connector=self.connector)
|
||||
self.device = Device.objects.create(
|
||||
name=generate_id(),
|
||||
identifier=generate_id(),
|
||||
)
|
||||
self.connection = AgentDeviceConnection.objects.create(
|
||||
device=self.device,
|
||||
connector=self.connector,
|
||||
apple_sign_key_id=self.apple_sign_key.kid,
|
||||
apple_signing_key=sign_key_pem,
|
||||
apple_encryption_key=self.enc_pub,
|
||||
)
|
||||
self.user = create_test_user()
|
||||
AgentDeviceUserBinding.objects.create(
|
||||
target=self.device,
|
||||
user=self.user,
|
||||
order=0,
|
||||
apple_enclave_key_id=self.apple_sign_key.kid,
|
||||
apple_secure_enclave_key=sign_key_pem,
|
||||
)
|
||||
self.device_token = DeviceToken.objects.create(device=self.connection)
|
||||
|
||||
@reconcile_app("authentik_crypto")
|
||||
def test_token(self):
|
||||
nonce = generate_id()
|
||||
AppleNonce.objects.create(
|
||||
device_token=self.device_token,
|
||||
nonce=nonce,
|
||||
)
|
||||
embedded = encode(
|
||||
{"iss": str(self.connector.pk), "aud": str(self.device.pk), "request_nonce": nonce},
|
||||
self.apple_sign_key.private_key,
|
||||
headers={
|
||||
"kid": self.apple_sign_key.kid,
|
||||
},
|
||||
algorithm=JWTAlgorithms.from_private_key(self.apple_sign_key.private_key),
|
||||
)
|
||||
assertion = encode(
|
||||
{
|
||||
"iss": str(self.connector.pk),
|
||||
"aud": "http://testserver/endpoints/agent/psso/token/",
|
||||
"request_nonce": nonce,
|
||||
"assertion": embedded,
|
||||
"jwe_crypto": {
|
||||
"apv": (
|
||||
"AAAABUFwcGxlAAAAQQTFgZOospN6KbkhXhx1lfa-AKYxjEfJhTJrkpdEY_srMmkPzS7VN0Bzt2AtNBEXE"
|
||||
"aphDONiP2Mq6Oxytv5JKOxHAAAAJDgyOThERkY5LTVFMUUtNEUwMS04OEUwLUI3QkQzOUM4QjA3Qw"
|
||||
)
|
||||
},
|
||||
},
|
||||
self.apple_sign_key.private_key,
|
||||
headers={
|
||||
"kid": self.apple_sign_key.kid,
|
||||
},
|
||||
algorithm=JWTAlgorithms.from_private_key(self.apple_sign_key.private_key),
|
||||
)
|
||||
res = self.client.post(
|
||||
reverse("authentik_enterprise_endpoints_connectors_agent:psso-token"),
|
||||
data={
|
||||
"assertion": assertion,
|
||||
"platform_sso_version": "1.0",
|
||||
"grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
|
||||
},
|
||||
)
|
||||
|
||||
self.assertEqual(res.status_code, 200)
|
||||
@@ -0,0 +1,111 @@
|
||||
from json import loads
|
||||
|
||||
from django.urls import reverse
|
||||
from django.utils.timezone import now
|
||||
from jwt import decode
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from authentik.blueprints.tests import reconcile_app
|
||||
from authentik.core.models import Group
|
||||
from authentik.core.tests.utils import create_test_user
|
||||
from authentik.endpoints.connectors.agent.api.connectors import AgentDeviceConnection
|
||||
from authentik.endpoints.connectors.agent.models import AgentConnector, EnrollmentToken
|
||||
from authentik.endpoints.models import Device, DeviceAccessGroup
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.policies.models import PolicyBinding
|
||||
from authentik.providers.oauth2.models import AccessToken, OAuth2Provider
|
||||
|
||||
|
||||
class TestConnectorAuthFed(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.connector = AgentConnector.objects.create(name=generate_id())
|
||||
self.token = EnrollmentToken.objects.create(name=generate_id(), connector=self.connector)
|
||||
self.device = Device.objects.create(
|
||||
name=generate_id(),
|
||||
identifier=generate_id(),
|
||||
)
|
||||
self.connection = AgentDeviceConnection.objects.create(
|
||||
device=self.device,
|
||||
connector=self.connector,
|
||||
)
|
||||
self.user = create_test_user()
|
||||
self.provider = OAuth2Provider.objects.create(name=generate_id())
|
||||
self.raw_token = self.provider.encode({"foo": "bar"})
|
||||
self.token = AccessToken.objects.create(
|
||||
provider=self.provider, user=self.user, token=self.raw_token, auth_time=now()
|
||||
)
|
||||
self.connector.jwt_federation_providers.add(self.provider)
|
||||
|
||||
@reconcile_app("authentik_crypto")
|
||||
def test_auth_fed(self):
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:agentconnector-auth-fed") + f"?device={self.device.name}",
|
||||
HTTP_AUTHORIZATION=f"Bearer {self.raw_token}",
|
||||
)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
@reconcile_app("authentik_crypto")
|
||||
def test_auth_fed_policy_group(self):
|
||||
device_group = DeviceAccessGroup.objects.create(name=generate_id())
|
||||
self.device.access_group = device_group
|
||||
self.device.save()
|
||||
|
||||
group = Group.objects.create(name=generate_id())
|
||||
group.users.add(self.user)
|
||||
|
||||
PolicyBinding.objects.create(target=device_group, group=group, order=0)
|
||||
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:agentconnector-auth-fed") + f"?device={self.device.name}",
|
||||
HTTP_AUTHORIZATION=f"Bearer {self.raw_token}",
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
res = loads(response.content)
|
||||
token = decode(res["token"], options={"verify_signature": False})
|
||||
self.assertEqual(token["iss"], "goauthentik.io/platform")
|
||||
self.assertEqual(token["aud"], str(self.device.pk))
|
||||
|
||||
@reconcile_app("authentik_crypto")
|
||||
def test_auth_fed_policy_group_deny(self):
|
||||
device_group = DeviceAccessGroup.objects.create(name=generate_id())
|
||||
self.device.access_group = device_group
|
||||
self.device.save()
|
||||
|
||||
group = Group.objects.create(name=generate_id())
|
||||
# group.users.add(self.user)
|
||||
|
||||
PolicyBinding.objects.create(target=device_group, group=group, order=0)
|
||||
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:agentconnector-auth-fed") + f"?device={self.device.name}",
|
||||
HTTP_AUTHORIZATION=f"Bearer {self.raw_token}",
|
||||
)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertJSONEqual(
|
||||
response.content,
|
||||
{
|
||||
"policy_result": "Policy denied access",
|
||||
"policy_messages": [],
|
||||
},
|
||||
)
|
||||
|
||||
@reconcile_app("authentik_crypto")
|
||||
def test_auth_fed_invalid(self):
|
||||
# No token
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:agentconnector-auth-fed") + f"?device={self.device.name}foo",
|
||||
)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
# No device
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:agentconnector-auth-fed") + f"?device={self.device.name}foo",
|
||||
HTTP_AUTHORIZATION=f"Bearer {self.raw_token}",
|
||||
)
|
||||
self.assertEqual(response.status_code, 404)
|
||||
# invalid token
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:agentconnector-auth-fed") + f"?device={self.device.name}",
|
||||
HTTP_AUTHORIZATION=f"Bearer {self.raw_token}aa",
|
||||
)
|
||||
self.assertEqual(response.status_code, 404)
|
||||
@@ -0,0 +1,128 @@
|
||||
from hashlib import sha256
|
||||
from json import loads
|
||||
from unittest.mock import MagicMock, patch
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
|
||||
from django.urls import reverse
|
||||
from jwt import decode
|
||||
|
||||
from authentik.blueprints.tests import reconcile_app
|
||||
from authentik.core.models import Group
|
||||
from authentik.core.tests.utils import create_test_flow, create_test_user
|
||||
from authentik.endpoints.connectors.agent.api.connectors import AgentDeviceConnection
|
||||
from authentik.endpoints.connectors.agent.models import AgentConnector, DeviceToken, EnrollmentToken
|
||||
from authentik.endpoints.models import Device, DeviceAccessGroup
|
||||
from authentik.enterprise.endpoints.connectors.agent.views.auth_interactive import QS_AGENT_IA_TOKEN
|
||||
from authentik.enterprise.license import LicenseKey
|
||||
from authentik.enterprise.models import License
|
||||
from authentik.enterprise.tests.test_license import expiry_valid
|
||||
from authentik.flows.tests import FlowTestCase
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.policies.models import PolicyBinding
|
||||
|
||||
|
||||
class TestConnectorAuthIA(FlowTestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.connector = AgentConnector.objects.create(
|
||||
name=generate_id(),
|
||||
authorization_flow=create_test_flow(),
|
||||
)
|
||||
self.token = EnrollmentToken.objects.create(name=generate_id(), connector=self.connector)
|
||||
self.device = Device.objects.create(
|
||||
name=generate_id(),
|
||||
identifier=generate_id(),
|
||||
)
|
||||
self.connection = AgentDeviceConnection.objects.create(
|
||||
device=self.device,
|
||||
connector=self.connector,
|
||||
)
|
||||
self.device_token = DeviceToken.objects.create(
|
||||
device=self.connection,
|
||||
key=generate_id(),
|
||||
)
|
||||
self.user = create_test_user()
|
||||
|
||||
@patch(
|
||||
"authentik.enterprise.license.LicenseKey.validate",
|
||||
MagicMock(
|
||||
return_value=LicenseKey(
|
||||
aud="",
|
||||
exp=expiry_valid,
|
||||
name=generate_id(),
|
||||
internal_users=100,
|
||||
external_users=100,
|
||||
)
|
||||
),
|
||||
)
|
||||
def test_auth_ia_initiate(self):
|
||||
License.objects.create(key=generate_id())
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:agentconnector-auth-ia"),
|
||||
HTTP_AUTHORIZATION=f"Bearer+agent {self.device_token.key}",
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
@reconcile_app("authentik_crypto")
|
||||
def test_auth_ia_fulfill(self):
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:agentconnector-auth-ia"),
|
||||
HTTP_AUTHORIZATION=f"Bearer+agent {self.device_token.key}",
|
||||
HTTP_X_AUTHENTIK_PLATFORM_AUTH_DTH=sha256(self.device_token.key.encode()).hexdigest(),
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
res = loads(response.content)
|
||||
response = self.client.get(
|
||||
res["url"],
|
||||
HTTP_AUTHORIZATION=f"Bearer+agent {self.device_token.key}",
|
||||
HTTP_X_AUTHENTIK_PLATFORM_AUTH_DTH=sha256(self.device_token.key.encode()).hexdigest(),
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn(b"Permission denied", response.content)
|
||||
|
||||
@patch(
|
||||
"authentik.enterprise.license.LicenseKey.validate",
|
||||
MagicMock(
|
||||
return_value=LicenseKey(
|
||||
aud="",
|
||||
exp=expiry_valid,
|
||||
name=generate_id(),
|
||||
internal_users=100,
|
||||
external_users=100,
|
||||
)
|
||||
),
|
||||
)
|
||||
@reconcile_app("authentik_crypto")
|
||||
def test_auth_ia_fulfill_policy(self):
|
||||
License.objects.create(key=generate_id())
|
||||
device_group = DeviceAccessGroup.objects.create(name=generate_id())
|
||||
self.device.access_group = device_group
|
||||
self.device.save()
|
||||
|
||||
group = Group.objects.create(name=generate_id())
|
||||
group.users.add(self.user)
|
||||
|
||||
PolicyBinding.objects.create(target=device_group, group=group, order=0)
|
||||
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:agentconnector-auth-ia"),
|
||||
HTTP_AUTHORIZATION=f"Bearer+agent {self.device_token.key}",
|
||||
HTTP_X_AUTHENTIK_PLATFORM_AUTH_DTH=sha256(self.device_token.key.encode()).hexdigest(),
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
res = loads(response.content)
|
||||
response = self.client.get(
|
||||
res["url"],
|
||||
HTTP_AUTHORIZATION=f"Bearer+agent {self.device_token.key}",
|
||||
HTTP_X_AUTHENTIK_PLATFORM_AUTH_DTH=sha256(self.device_token.key.encode()).hexdigest(),
|
||||
)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
url = urlparse(response.url)
|
||||
self.assertEqual(url.scheme, "goauthentik.io")
|
||||
qs = parse_qs(url.query)
|
||||
raw_token = qs[QS_AGENT_IA_TOKEN][0]
|
||||
token = decode(raw_token.encode(), options={"verify_signature": False})
|
||||
self.assertEqual(token["iss"], "goauthentik.io/platform")
|
||||
self.assertEqual(token["aud"], str(self.device.pk))
|
||||
36
authentik/enterprise/endpoints/connectors/agent/urls.py
Normal file
36
authentik/enterprise/endpoints/connectors/agent/urls.py
Normal file
@@ -0,0 +1,36 @@
|
||||
from django.urls import path
|
||||
|
||||
from authentik.enterprise.endpoints.connectors.agent.views.apple_jwks import AppleJWKSView
|
||||
from authentik.enterprise.endpoints.connectors.agent.views.apple_nonce import NonceView
|
||||
from authentik.enterprise.endpoints.connectors.agent.views.apple_register import (
|
||||
RegisterDeviceView,
|
||||
RegisterUserView,
|
||||
)
|
||||
from authentik.enterprise.endpoints.connectors.agent.views.apple_token import TokenView
|
||||
from authentik.enterprise.endpoints.connectors.agent.views.auth_interactive import (
|
||||
AgentInteractiveAuth,
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
path(
|
||||
"authenticate/<uuid:token_uuid>/",
|
||||
AgentInteractiveAuth.as_view(),
|
||||
name="authenticate",
|
||||
),
|
||||
path("psso/token/", TokenView.as_view(), name="psso-token"),
|
||||
path("psso/jwks/", AppleJWKSView.as_view(), name="psso-jwks"),
|
||||
path("psso/nonce/", NonceView.as_view(), name="psso-nonce"),
|
||||
]
|
||||
|
||||
api_urlpatterns = [
|
||||
path(
|
||||
"endpoints/agents/psso/register/device/",
|
||||
RegisterDeviceView.as_view(),
|
||||
name="psso-register-device",
|
||||
),
|
||||
path(
|
||||
"endpoints/agents/psso/register/user/",
|
||||
RegisterUserView.as_view(),
|
||||
name="psso-register-user",
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,14 @@
|
||||
from django.http import Http404
|
||||
|
||||
from authentik.crypto.apps import MANAGED_KEY
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.providers.oauth2.views.jwks import JWKSView
|
||||
|
||||
|
||||
class AppleJWKSView(JWKSView):
|
||||
|
||||
def get_keys(self):
|
||||
kp = CertificateKeyPair.objects.filter(managed=MANAGED_KEY).first()
|
||||
if not kp:
|
||||
raise Http404
|
||||
yield self.get_jwk_for_key(kp, "sig")
|
||||
@@ -0,0 +1,32 @@
|
||||
from base64 import b64encode
|
||||
from datetime import timedelta
|
||||
from secrets import token_bytes
|
||||
from urllib.parse import unquote
|
||||
|
||||
from django.http import HttpRequest, HttpResponseBadRequest, JsonResponse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils.timezone import now
|
||||
from django.views import View
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
|
||||
from authentik.endpoints.connectors.agent.models import AppleNonce, DeviceToken
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name="dispatch")
|
||||
class NonceView(View):
|
||||
|
||||
def post(self, request: HttpRequest, *args, **kwargs):
|
||||
raw_token = unquote(self.request.POST.get("x-ak-device-token"))
|
||||
device_token = DeviceToken.filter_not_expired(key=raw_token).first()
|
||||
if not device_token:
|
||||
return HttpResponseBadRequest()
|
||||
nonce = AppleNonce.objects.create(
|
||||
nonce=b64encode(token_bytes(32)).decode(),
|
||||
expires=now() + timedelta(minutes=5),
|
||||
device_token=device_token,
|
||||
)
|
||||
return JsonResponse(
|
||||
{
|
||||
"Nonce": nonce.nonce,
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,131 @@
|
||||
from django.urls import reverse
|
||||
from drf_spectacular.utils import extend_schema
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.fields import CharField
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from authentik.api.validation import validate
|
||||
from authentik.core.api.users import UserSelfSerializer
|
||||
from authentik.core.api.utils import PassiveSerializer
|
||||
from authentik.endpoints.connectors.agent.auth import AgentAuth
|
||||
from authentik.endpoints.connectors.agent.models import (
|
||||
AgentDeviceConnection,
|
||||
AgentDeviceUserBinding,
|
||||
DeviceAuthenticationToken,
|
||||
DeviceToken,
|
||||
)
|
||||
from authentik.enterprise.api import EnterpriseRequiredMixin
|
||||
from authentik.lib.generators import generate_key
|
||||
|
||||
|
||||
class RegisterDeviceView(APIView):
|
||||
|
||||
class AgentPSSODeviceRegistration(EnterpriseRequiredMixin, PassiveSerializer):
|
||||
"""Register Apple device via Platform SSO"""
|
||||
|
||||
device_signing_key = CharField()
|
||||
device_encryption_key = CharField()
|
||||
sign_key_id = CharField()
|
||||
enc_key_id = CharField()
|
||||
|
||||
class AgentPSSODeviceRegistrationResponse(PassiveSerializer):
|
||||
"""authentik settings for Platform SSO tokens"""
|
||||
|
||||
client_id = CharField()
|
||||
issuer = CharField()
|
||||
token_endpoint = CharField()
|
||||
jwks_endpoint = CharField()
|
||||
audience = CharField()
|
||||
nonce_endpoint = CharField()
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
pagination_class = None
|
||||
filter_backends = []
|
||||
serializer_class = AgentPSSODeviceRegistration
|
||||
authentication_classes = [AgentAuth]
|
||||
|
||||
@extend_schema(
|
||||
responses={
|
||||
200: AgentPSSODeviceRegistrationResponse(),
|
||||
}
|
||||
)
|
||||
@validate(AgentPSSODeviceRegistration)
|
||||
def post(self, request: Request, body: AgentPSSODeviceRegistration) -> Response:
|
||||
device_token: DeviceToken = request.auth
|
||||
conn: AgentDeviceConnection = device_token.device
|
||||
conn.apple_signing_key = body.validated_data["device_signing_key"]
|
||||
conn.apple_encryption_key = body.validated_data["device_encryption_key"]
|
||||
conn.apple_sign_key_id = body.validated_data["sign_key_id"]
|
||||
conn.apple_enc_key_id = body.validated_data["enc_key_id"]
|
||||
conn.apple_key_exchange_key = generate_key()
|
||||
conn.save()
|
||||
return Response(
|
||||
data={
|
||||
"client_id": str(conn.connector.pk),
|
||||
"issuer": self.request.build_absolute_uri(
|
||||
reverse("authentik_enterprise_endpoints_connectors_agent:psso-token")
|
||||
),
|
||||
"audience": str(conn.device.pk),
|
||||
"token_endpoint": request.build_absolute_uri(
|
||||
reverse("authentik_enterprise_endpoints_connectors_agent:psso-token")
|
||||
),
|
||||
"jwks_endpoint": request.build_absolute_uri(
|
||||
reverse("authentik_enterprise_endpoints_connectors_agent:psso-jwks")
|
||||
),
|
||||
"nonce_endpoint": request.build_absolute_uri(
|
||||
reverse("authentik_enterprise_endpoints_connectors_agent:psso-nonce")
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class RegisterUserView(APIView):
|
||||
|
||||
class AgentPSSOUserRegistration(EnterpriseRequiredMixin, PassiveSerializer):
|
||||
"""Register Apple device user via Platform SSO"""
|
||||
|
||||
user_auth = CharField()
|
||||
user_secure_enclave_key = CharField()
|
||||
enclave_key_id = CharField()
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
pagination_class = None
|
||||
filter_backends = []
|
||||
serializer_class = AgentPSSOUserRegistration
|
||||
authentication_classes = [AgentAuth]
|
||||
|
||||
@extend_schema(
|
||||
responses={
|
||||
200: UserSelfSerializer(),
|
||||
}
|
||||
)
|
||||
@validate(AgentPSSOUserRegistration)
|
||||
def post(self, request: Request, body: AgentPSSOUserRegistration) -> Response:
|
||||
device_token: DeviceToken = request.auth
|
||||
conn: AgentDeviceConnection = device_token.device
|
||||
user_token = DeviceAuthenticationToken.filter_not_expired(
|
||||
device=conn.device,
|
||||
token=body.validated_data["user_auth"],
|
||||
device_token=device_token,
|
||||
).first()
|
||||
if not user_token:
|
||||
raise ValidationError("Invalid user authentication")
|
||||
AgentDeviceUserBinding.objects.update_or_create(
|
||||
target=conn.device,
|
||||
user=user_token.user,
|
||||
connector=conn.connector,
|
||||
create_defaults={
|
||||
"is_primary": True,
|
||||
"order": 0,
|
||||
},
|
||||
defaults={
|
||||
"apple_secure_enclave_key": body.validated_data["user_secure_enclave_key"],
|
||||
"apple_enclave_key_id": body.validated_data["enclave_key_id"],
|
||||
},
|
||||
)
|
||||
return Response(
|
||||
UserSelfSerializer(instance=user_token.user, context={"request": request}).data
|
||||
)
|
||||
@@ -0,0 +1,185 @@
|
||||
from typing import Any
|
||||
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.urls import reverse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils.timezone import now
|
||||
from django.views import View
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from jwt import PyJWTError, decode, encode, get_unverified_header
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.core.models import AuthenticatedSession, Session, User
|
||||
from authentik.core.sessions import SessionStore
|
||||
from authentik.crypto.apps import MANAGED_KEY
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.endpoints.connectors.agent.models import (
|
||||
AgentConnector,
|
||||
AgentDeviceConnection,
|
||||
AgentDeviceUserBinding,
|
||||
AppleNonce,
|
||||
DeviceAuthenticationToken,
|
||||
)
|
||||
from authentik.enterprise.endpoints.connectors.agent.http import JWEResponse
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.events.signals import SESSION_LOGIN_EVENT
|
||||
from authentik.lib.utils.time import timedelta_from_string
|
||||
from authentik.providers.oauth2.constants import TOKEN_TYPE
|
||||
from authentik.providers.oauth2.id_token import IDToken
|
||||
from authentik.providers.oauth2.models import JWTAlgorithms
|
||||
from authentik.root.middleware import SessionMiddleware
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name="dispatch")
|
||||
class TokenView(View):
|
||||
|
||||
device_connection: AgentDeviceConnection
|
||||
connector: AgentConnector
|
||||
|
||||
def post(self, request: HttpRequest) -> HttpResponse:
|
||||
assertion = request.POST.get("assertion", request.POST.get("request"))
|
||||
if not assertion:
|
||||
return HttpResponse(status=400)
|
||||
self.now = now()
|
||||
try:
|
||||
self.jwt_request = self.validate_request_token(assertion)
|
||||
except PyJWTError as exc:
|
||||
LOGGER.warning("failed to parse JWT", exc=exc)
|
||||
raise ValidationError("Invalid request") from None
|
||||
version = request.POST.get("platform_sso_version")
|
||||
grant_type = request.POST.get("grant_type")
|
||||
handler_func = (
|
||||
f"handle_v{version}_{grant_type}".replace("-", "_")
|
||||
.replace("+", "_")
|
||||
.replace(":", "_")
|
||||
.replace(".", "_")
|
||||
)
|
||||
handler = getattr(self, handler_func, None)
|
||||
if not handler:
|
||||
LOGGER.debug("Handler not found", handler=handler_func)
|
||||
return HttpResponse(status=400)
|
||||
LOGGER.debug("sending to handler", handler=handler_func)
|
||||
return handler()
|
||||
|
||||
def validate_request_token(self, assertion: str) -> dict[str, Any]:
|
||||
# Decode without validation to get header
|
||||
header = get_unverified_header(assertion)
|
||||
LOGGER.debug("token header", header=header)
|
||||
expected_kid = header["kid"]
|
||||
|
||||
self.device_connection = (
|
||||
AgentDeviceConnection.objects.filter(apple_sign_key_id=expected_kid)
|
||||
.select_related("device")
|
||||
.first()
|
||||
)
|
||||
self.connector = AgentConnector.objects.get(pk=self.device_connection.connector.pk)
|
||||
LOGGER.debug("got device", device=self.device_connection.device)
|
||||
|
||||
expected_aud = self.request.build_absolute_uri(
|
||||
reverse("authentik_enterprise_endpoints_connectors_agent:psso-token")
|
||||
)
|
||||
if not self.device_connection.apple_signing_key:
|
||||
LOGGER.warning("Failed to issue token for device, no apple_signing_key")
|
||||
raise ValidationError("Invalid request")
|
||||
# Properly decode the JWT with the key from the device
|
||||
decoded = decode(
|
||||
assertion,
|
||||
self.device_connection.apple_signing_key,
|
||||
algorithms=["ES256"],
|
||||
audience=expected_aud,
|
||||
issuer=str(self.connector.pk),
|
||||
)
|
||||
self.remote_nonce = decoded.get("nonce")
|
||||
|
||||
# Check that the nonce hasn't been used before
|
||||
nonce = AppleNonce.filter_not_expired(nonce=decoded["request_nonce"]).first()
|
||||
if not nonce:
|
||||
raise ValidationError("Invalid nonce")
|
||||
self.nonce = nonce
|
||||
nonce.delete()
|
||||
return decoded
|
||||
|
||||
def validate_embedded_assertion(self, assertion: str) -> tuple[AgentDeviceUserBinding, dict]:
|
||||
"""Decode an embedded assertion and validate it by looking up the matching device user"""
|
||||
decode_unvalidated = get_unverified_header(assertion)
|
||||
expected_kid = decode_unvalidated["kid"]
|
||||
|
||||
device_user = AgentDeviceUserBinding.objects.filter(
|
||||
target=self.device_connection.device, apple_enclave_key_id=expected_kid
|
||||
).first()
|
||||
if not device_user:
|
||||
LOGGER.warning("Could not find device user binding for user")
|
||||
raise ValidationError("Invalid request")
|
||||
decoded: dict[str, Any] = decode(
|
||||
assertion,
|
||||
device_user.apple_secure_enclave_key,
|
||||
audience=str(self.device_connection.device.pk),
|
||||
algorithms=["ES256"],
|
||||
)
|
||||
if decoded.get("nonce") != self.jwt_request.get("nonce"):
|
||||
LOGGER.warning("Mis-matched nonce to outer assertion")
|
||||
raise ValidationError("Invalid nonce")
|
||||
return device_user, decoded
|
||||
|
||||
def create_auth_session(self, user: User):
|
||||
event = Event.new(EventAction.LOGIN).from_http(self.request, user=user)
|
||||
store = SessionStore()
|
||||
store[SESSION_LOGIN_EVENT] = event
|
||||
store.save()
|
||||
session = Session.objects.filter(session_key=store.session_key).first()
|
||||
session.expires = self.now + timedelta_from_string(self.connector.auth_session_duration)
|
||||
AuthenticatedSession.objects.create(session=session, user=user)
|
||||
session = SessionMiddleware.encode_session(store.session_key, user)
|
||||
return session
|
||||
|
||||
def create_id_token(self, user: User, **kwargs):
|
||||
issuer = self.request.build_absolute_uri(
|
||||
reverse("authentik_enterprise_endpoints_connectors_agent:psso-token")
|
||||
)
|
||||
id_token = IDToken(
|
||||
iss=issuer,
|
||||
sub=user.username,
|
||||
aud=str(self.connector.pk),
|
||||
exp=int(
|
||||
(self.now + timedelta_from_string(self.connector.auth_session_duration)).timestamp()
|
||||
),
|
||||
iat=int(now().timestamp()),
|
||||
**kwargs,
|
||||
)
|
||||
kp = CertificateKeyPair.objects.filter(managed=MANAGED_KEY).first()
|
||||
return encode(
|
||||
id_token.to_dict(),
|
||||
kp.private_key,
|
||||
headers={
|
||||
"kid": kp.kid,
|
||||
},
|
||||
algorithm=JWTAlgorithms.from_private_key(kp.private_key),
|
||||
)
|
||||
|
||||
def handle_v1_0_urn_ietf_params_oauth_grant_type_jwt_bearer(self):
|
||||
try:
|
||||
user, inner = self.validate_embedded_assertion(self.jwt_request["assertion"])
|
||||
except PyJWTError as exc:
|
||||
LOGGER.warning("failed to validate inner assertion", exc=exc)
|
||||
raise ValidationError("Invalid request") from None
|
||||
id_token = self.create_id_token(user.user)
|
||||
auth_token = DeviceAuthenticationToken.objects.create(
|
||||
device=self.device_connection.device,
|
||||
connector=self.connector,
|
||||
user=user.user,
|
||||
device_token=self.nonce.device_token,
|
||||
)
|
||||
return JWEResponse(
|
||||
{
|
||||
"refresh_token": auth_token.token,
|
||||
"refresh_token_expires_in": int((auth_token.expires - now()).total_seconds()),
|
||||
"id_token": id_token,
|
||||
"token_type": TOKEN_TYPE,
|
||||
"session_key": self.create_auth_session(user.user),
|
||||
},
|
||||
device=self.device_connection,
|
||||
apv=self.jwt_request["jwe_crypto"]["apv"],
|
||||
)
|
||||
@@ -0,0 +1,111 @@
|
||||
from hashlib import sha256
|
||||
from hmac import compare_digest
|
||||
|
||||
from django.http import Http404, HttpRequest, HttpResponse, HttpResponseBadRequest, QueryDict
|
||||
|
||||
from authentik.endpoints.connectors.agent.models import AgentConnector, DeviceAuthenticationToken
|
||||
from authentik.endpoints.models import Device
|
||||
from authentik.enterprise.endpoints.connectors.agent.auth import (
|
||||
agent_auth_issue_token,
|
||||
check_device_policies,
|
||||
)
|
||||
from authentik.enterprise.policy import EnterprisePolicyAccessView
|
||||
from authentik.flows.exceptions import FlowNonApplicableException
|
||||
from authentik.flows.models import in_memory_stage
|
||||
from authentik.flows.planner import PLAN_CONTEXT_DEVICE, FlowPlanner
|
||||
from authentik.flows.stage import StageView
|
||||
from authentik.providers.oauth2.utils import HttpResponseRedirectScheme
|
||||
|
||||
PLAN_CONTEXT_DEVICE_AUTH_TOKEN = "goauthentik.io/endpoints/device_auth_token" # nosec
|
||||
|
||||
QS_AGENT_IA_TOKEN = "ak-auth-ia-token" # nosec
|
||||
|
||||
|
||||
class AgentInteractiveAuth(EnterprisePolicyAccessView):
|
||||
"""Agent device authentication"""
|
||||
|
||||
auth_token: DeviceAuthenticationToken
|
||||
device: Device
|
||||
connector: AgentConnector
|
||||
|
||||
def resolve_provider_application(self):
|
||||
auth_token = (
|
||||
DeviceAuthenticationToken.filter_not_expired(identifier=self.kwargs["token_uuid"])
|
||||
.prefetch_related()
|
||||
.first()
|
||||
)
|
||||
if not auth_token:
|
||||
raise Http404
|
||||
self.auth_token = auth_token
|
||||
self.device = auth_token.device
|
||||
self.connector = auth_token.connector.agentconnector
|
||||
|
||||
def user_has_access(self, user=None, pbm=None):
|
||||
enterprise_result = self.check_license()
|
||||
if not enterprise_result.passing:
|
||||
return enterprise_result
|
||||
return check_device_policies(self.device, user or self.request.user, self.request)
|
||||
|
||||
def modify_flow_context(self, flow, context):
|
||||
return {
|
||||
PLAN_CONTEXT_DEVICE: self.device,
|
||||
**context,
|
||||
}
|
||||
|
||||
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||
device_token_hash = request.headers.get("X-Authentik-Platform-Auth-DTH")
|
||||
if not device_token_hash:
|
||||
return HttpResponseBadRequest("Invalid device token")
|
||||
if not compare_digest(
|
||||
device_token_hash, sha256(self.auth_token.device_token.key.encode()).hexdigest()
|
||||
):
|
||||
return HttpResponseBadRequest("Invalid device token")
|
||||
|
||||
planner = FlowPlanner(self.connector.authorization_flow)
|
||||
planner.allow_empty_flows = True
|
||||
try:
|
||||
plan = planner.plan(
|
||||
self.request,
|
||||
{
|
||||
PLAN_CONTEXT_DEVICE: self.device,
|
||||
PLAN_CONTEXT_DEVICE_AUTH_TOKEN: self.auth_token,
|
||||
},
|
||||
)
|
||||
except FlowNonApplicableException:
|
||||
return self.handle_no_permission_authenticated()
|
||||
plan.append_stage(in_memory_stage(AgentAuthFulfillmentStage))
|
||||
|
||||
return plan.to_redirect(
|
||||
self.request,
|
||||
self.connector.authorization_flow,
|
||||
allowed_silent_types=[AgentAuthFulfillmentStage],
|
||||
)
|
||||
|
||||
|
||||
class AgentAuthFulfillmentStage(StageView):
|
||||
|
||||
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||
device: Device = self.executor.plan.context.pop(PLAN_CONTEXT_DEVICE)
|
||||
auth_token: DeviceAuthenticationToken = self.executor.plan.context.pop(
|
||||
PLAN_CONTEXT_DEVICE_AUTH_TOKEN
|
||||
)
|
||||
|
||||
token, exp = agent_auth_issue_token(
|
||||
device,
|
||||
auth_token.connector.agentconnector,
|
||||
request.user,
|
||||
jti=str(auth_token.identifier),
|
||||
)
|
||||
if not token or not exp:
|
||||
return self.executor.stage_invalid("Failed to generate token")
|
||||
auth_token.user = request.user
|
||||
auth_token.token = token
|
||||
auth_token.expires = exp
|
||||
auth_token.expiring = True
|
||||
auth_token.save()
|
||||
qd = QueryDict(mutable=True)
|
||||
qd[QS_AGENT_IA_TOKEN] = token
|
||||
return HttpResponseRedirectScheme(
|
||||
"goauthentik.io://platform/finished?" + qd.urlencode(),
|
||||
allowed_schemes=["goauthentik.io"],
|
||||
)
|
||||
@@ -4,7 +4,7 @@ from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from authentik.core.models import User, UserTypes
|
||||
from authentik.enterprise.license import LicenseKey
|
||||
from authentik.policies.types import PolicyRequest, PolicyResult
|
||||
from authentik.policies.types import PolicyResult
|
||||
from authentik.policies.views import PolicyAccessView
|
||||
|
||||
|
||||
@@ -21,8 +21,6 @@ class EnterprisePolicyAccessView(PolicyAccessView):
|
||||
|
||||
def user_has_access(self, user: User | None = None) -> PolicyResult:
|
||||
user = user or self.request.user
|
||||
request = PolicyRequest(user)
|
||||
request.http_request = self.request
|
||||
result = super().user_has_access(user)
|
||||
enterprise_result = self.check_license()
|
||||
if not enterprise_result.passing:
|
||||
|
||||
@@ -14,7 +14,12 @@ from authentik.core.models import AuthenticatedSession, User
|
||||
from authentik.core.signals import login_failed, password_changed
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.flows.models import Stage
|
||||
from authentik.flows.planner import PLAN_CONTEXT_OUTPOST, PLAN_CONTEXT_SOURCE, FlowPlan
|
||||
from authentik.flows.planner import (
|
||||
PLAN_CONTEXT_DEVICE,
|
||||
PLAN_CONTEXT_OUTPOST,
|
||||
PLAN_CONTEXT_SOURCE,
|
||||
FlowPlan,
|
||||
)
|
||||
from authentik.flows.views.executor import SESSION_KEY_PLAN
|
||||
from authentik.stages.invitation.models import Invitation
|
||||
from authentik.stages.invitation.signals import invitation_used
|
||||
@@ -42,6 +47,9 @@ def on_user_logged_in(sender, request: HttpRequest, user: User, **_):
|
||||
if PLAN_CONTEXT_OUTPOST in flow_plan.context:
|
||||
# Save outpost context
|
||||
kwargs[PLAN_CONTEXT_OUTPOST] = flow_plan.context[PLAN_CONTEXT_OUTPOST]
|
||||
if PLAN_CONTEXT_DEVICE in flow_plan.context:
|
||||
# Save device
|
||||
kwargs[PLAN_CONTEXT_DEVICE] = flow_plan.context[PLAN_CONTEXT_DEVICE]
|
||||
event = Event.new(EventAction.LOGIN, **kwargs).from_http(request, user=user)
|
||||
request.session[SESSION_LOGIN_EVENT] = event
|
||||
request.session.save()
|
||||
|
||||
@@ -37,6 +37,7 @@ PLAN_CONTEXT_PENDING_USER = "pending_user"
|
||||
PLAN_CONTEXT_SSO = "is_sso"
|
||||
PLAN_CONTEXT_REDIRECT = "redirect"
|
||||
PLAN_CONTEXT_APPLICATION = "application"
|
||||
PLAN_CONTEXT_DEVICE = "device"
|
||||
PLAN_CONTEXT_SOURCE = "source"
|
||||
PLAN_CONTEXT_OUTPOST = "outpost"
|
||||
PLAN_CONTEXT_POST = "goauthentik.io/http/post"
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
"""authentik OAuth2 JWKS Views"""
|
||||
|
||||
from base64 import b64encode, urlsafe_b64encode
|
||||
from collections.abc import Generator
|
||||
from typing import Literal
|
||||
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
from cryptography.hazmat.primitives.asymmetric.ec import (
|
||||
@@ -64,7 +66,7 @@ class JWKSView(View):
|
||||
"""Show RSA Key data for Provider"""
|
||||
|
||||
@staticmethod
|
||||
def get_jwk_for_key(key: CertificateKeyPair, use: str) -> dict | None:
|
||||
def get_jwk_for_key(key: CertificateKeyPair, use: Literal["sig", "enc"]) -> dict | None:
|
||||
"""Convert a certificate-key pair into JWK"""
|
||||
private_key = key.private_key
|
||||
key_data = None
|
||||
@@ -112,10 +114,9 @@ class JWKSView(View):
|
||||
)
|
||||
return key_data
|
||||
|
||||
def get(self, request: HttpRequest, application_slug: str) -> HttpResponse:
|
||||
"""Show JWK Key data for Provider"""
|
||||
def get_keys(self) -> Generator[dict | None]:
|
||||
provider_ids = Application.objects.filter(
|
||||
slug=application_slug,
|
||||
slug=self.kwargs["application_slug"],
|
||||
).values_list(
|
||||
"provider_id",
|
||||
flat=True,
|
||||
@@ -129,15 +130,15 @@ class JWKSView(View):
|
||||
if provider is None:
|
||||
raise Http404()
|
||||
|
||||
response_data = {}
|
||||
|
||||
if signing_key := provider.signing_key:
|
||||
jwk = JWKSView.get_jwk_for_key(signing_key, "sig")
|
||||
if jwk:
|
||||
response_data.setdefault("keys", [])
|
||||
response_data["keys"].append(jwk)
|
||||
yield JWKSView.get_jwk_for_key(signing_key, "sig")
|
||||
if encryption_key := provider.encryption_key:
|
||||
jwk = JWKSView.get_jwk_for_key(encryption_key, "enc")
|
||||
yield JWKSView.get_jwk_for_key(encryption_key, "enc")
|
||||
|
||||
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||
"""Show JWK Key data for Provider"""
|
||||
response_data = {}
|
||||
for jwk in self.get_keys():
|
||||
if jwk:
|
||||
response_data.setdefault("keys", [])
|
||||
response_data["keys"].append(jwk)
|
||||
|
||||
@@ -557,6 +557,8 @@ class TokenView(View):
|
||||
|
||||
provider: OAuth2Provider | None = None
|
||||
params: TokenParams | None = None
|
||||
params_class = TokenParams
|
||||
provider_class = OAuth2Provider
|
||||
|
||||
def dispatch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
|
||||
response = super().dispatch(request, *args, **kwargs)
|
||||
@@ -576,12 +578,14 @@ class TokenView(View):
|
||||
op="authentik.providers.oauth2.post.parse",
|
||||
):
|
||||
client_id, client_secret = extract_client_auth(request)
|
||||
self.provider = OAuth2Provider.objects.filter(client_id=client_id).first()
|
||||
self.provider = self.provider_class.objects.filter(client_id=client_id).first()
|
||||
if not self.provider:
|
||||
LOGGER.warning("OAuth2Provider does not exist", client_id=client_id)
|
||||
raise TokenError("invalid_client")
|
||||
CTX_AUTH_VIA.set("oauth_client_secret")
|
||||
self.params = TokenParams.parse(request, self.provider, client_id, client_secret)
|
||||
self.params = self.params_class.parse(
|
||||
request, self.provider, client_id, client_secret
|
||||
)
|
||||
|
||||
with start_span(
|
||||
op="authentik.providers.oauth2.post.response",
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
"""Group client"""
|
||||
|
||||
from itertools import batched
|
||||
from typing import Any
|
||||
|
||||
from django.db import transaction
|
||||
from orjson import dumps
|
||||
from pydantic import ValidationError
|
||||
from pydanticscim.group import GroupMember
|
||||
|
||||
from authentik.core.models import Group
|
||||
from authentik.lib.merge import MERGE_LIST_UNIQUE
|
||||
from authentik.lib.sync.mapper import PropertyMappingManager
|
||||
from authentik.lib.sync.outgoing.base import Direction
|
||||
from authentik.lib.sync.outgoing.exceptions import (
|
||||
@@ -114,10 +117,23 @@ class SCIMGroupClient(SCIMClient[Group, SCIMProviderGroup, SCIMGroupSchema]):
|
||||
self._patch_add_users(connection, users)
|
||||
return connection
|
||||
|
||||
def diff(self, local_created: dict[str, Any], connection: SCIMProviderUser):
|
||||
"""Check if a group is different than what we last wrote to the remote system.
|
||||
Returns true if there is a difference in data."""
|
||||
local_known = connection.attributes
|
||||
local_updated = {}
|
||||
MERGE_LIST_UNIQUE.merge(local_updated, local_known)
|
||||
MERGE_LIST_UNIQUE.merge(local_updated, local_created)
|
||||
return dumps(local_updated) != dumps(local_known)
|
||||
|
||||
def update(self, group: Group, connection: SCIMProviderGroup):
|
||||
"""Update existing group"""
|
||||
scim_group = self.to_schema(group, connection)
|
||||
scim_group.id = connection.scim_id
|
||||
payload = scim_group.model_dump(mode="json", exclude_unset=True)
|
||||
if not self.diff(payload, connection):
|
||||
self.logger.debug("Skipping group write as data has not changed")
|
||||
return self.patch_compare_users(group)
|
||||
try:
|
||||
if self._config.patch.supported:
|
||||
return self._update_patch(group, scim_group, connection)
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
"""User client"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from django.db import transaction
|
||||
from django.utils.http import urlencode
|
||||
from orjson import dumps
|
||||
from pydantic import ValidationError
|
||||
|
||||
from authentik.core.models import User
|
||||
from authentik.lib.merge import MERGE_LIST_UNIQUE
|
||||
from authentik.lib.sync.mapper import PropertyMappingManager
|
||||
from authentik.lib.sync.outgoing.exceptions import ObjectExistsSyncException, StopSync
|
||||
from authentik.policies.utils import delete_none_values
|
||||
@@ -92,17 +96,30 @@ class SCIMUserClient(SCIMClient[User, SCIMProviderUser, SCIMUserSchema]):
|
||||
provider=self.provider, user=user, scim_id=scim_id, attributes=response
|
||||
)
|
||||
|
||||
def diff(self, local_created: dict[str, Any], connection: SCIMProviderUser):
|
||||
"""Check if a user is different than what we last wrote to the remote system.
|
||||
Returns true if there is a difference in data."""
|
||||
local_known = connection.attributes
|
||||
local_updated = {}
|
||||
MERGE_LIST_UNIQUE.merge(local_updated, local_known)
|
||||
MERGE_LIST_UNIQUE.merge(local_updated, local_created)
|
||||
return dumps(local_updated) != dumps(local_known)
|
||||
|
||||
def update(self, user: User, connection: SCIMProviderUser):
|
||||
"""Update existing user"""
|
||||
scim_user = self.to_schema(user, connection)
|
||||
scim_user.id = connection.scim_id
|
||||
payload = scim_user.model_dump(
|
||||
mode="json",
|
||||
exclude_unset=True,
|
||||
)
|
||||
if not self.diff(payload, connection):
|
||||
self.logger.debug("Skipping user write as data has not changed")
|
||||
return
|
||||
response = self._request(
|
||||
"PUT",
|
||||
f"/Users/{connection.scim_id}",
|
||||
json=scim_user.model_dump(
|
||||
mode="json",
|
||||
exclude_unset=True,
|
||||
),
|
||||
json=payload,
|
||||
)
|
||||
connection.attributes = response
|
||||
connection.save()
|
||||
|
||||
@@ -9,7 +9,7 @@ from requests_mock import Mocker
|
||||
from authentik.blueprints.tests import apply_blueprint
|
||||
from authentik.core.models import Application, Group, User
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.providers.scim.models import SCIMMapping, SCIMProvider
|
||||
from authentik.providers.scim.models import SCIMMapping, SCIMProvider, SCIMProviderGroup
|
||||
|
||||
|
||||
class SCIMGroupTests(TestCase):
|
||||
@@ -106,6 +106,7 @@ class SCIMGroupTests(TestCase):
|
||||
"displayName": group.name,
|
||||
},
|
||||
)
|
||||
group.name = generate_id()
|
||||
group.save()
|
||||
self.assertEqual(mock.call_count, 4)
|
||||
self.assertEqual(mock.request_history[0].method, "GET")
|
||||
@@ -148,3 +149,56 @@ class SCIMGroupTests(TestCase):
|
||||
self.assertEqual(mock.request_history[0].method, "GET")
|
||||
self.assertEqual(mock.request_history[3].method, "DELETE")
|
||||
self.assertEqual(mock.request_history[3].url, f"https://localhost/Groups/{scim_id}")
|
||||
|
||||
@Mocker()
|
||||
def test_group_create_update_noop(self, mock: Mocker):
|
||||
"""Test group creation and update"""
|
||||
scim_id = generate_id()
|
||||
mock.get(
|
||||
"https://localhost/ServiceProviderConfig",
|
||||
json={},
|
||||
)
|
||||
mock.post(
|
||||
"https://localhost/Groups",
|
||||
json={
|
||||
"id": scim_id,
|
||||
},
|
||||
)
|
||||
mock.put(
|
||||
"https://localhost/Groups",
|
||||
json={
|
||||
"id": scim_id,
|
||||
},
|
||||
)
|
||||
uid = generate_id()
|
||||
group = Group.objects.create(
|
||||
name=uid,
|
||||
)
|
||||
self.assertEqual(mock.call_count, 2)
|
||||
self.assertEqual(mock.request_history[0].method, "GET")
|
||||
self.assertEqual(mock.request_history[1].method, "POST")
|
||||
body = loads(mock.request_history[1].body)
|
||||
with open("schemas/scim-group.schema.json", encoding="utf-8") as schema:
|
||||
validate(body, loads(schema.read()))
|
||||
self.assertEqual(
|
||||
body,
|
||||
{
|
||||
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group"],
|
||||
"externalId": str(group.pk),
|
||||
"displayName": group.name,
|
||||
},
|
||||
)
|
||||
conn = SCIMProviderGroup.objects.filter(group=group).first()
|
||||
conn.attributes = {
|
||||
"id": scim_id,
|
||||
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group"],
|
||||
"externalId": str(group.pk),
|
||||
"displayName": group.name,
|
||||
}
|
||||
conn.save()
|
||||
group.save()
|
||||
self.assertEqual(mock.call_count, 4)
|
||||
self.assertEqual(mock.request_history[0].method, "GET")
|
||||
self.assertEqual(mock.request_history[1].method, "POST")
|
||||
self.assertEqual(mock.request_history[2].method, "GET")
|
||||
self.assertEqual(mock.request_history[2].method, "GET")
|
||||
|
||||
@@ -10,7 +10,7 @@ from authentik.blueprints.tests import apply_blueprint
|
||||
from authentik.core.models import Application, Group, User
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.lib.sync.outgoing.base import SAFE_METHODS
|
||||
from authentik.providers.scim.models import SCIMMapping, SCIMProvider
|
||||
from authentik.providers.scim.models import SCIMMapping, SCIMProvider, SCIMProviderUser
|
||||
from authentik.providers.scim.tasks import scim_sync, scim_sync_objects
|
||||
from authentik.tasks.models import Task
|
||||
from authentik.tenants.models import Tenant
|
||||
@@ -273,6 +273,8 @@ class SCIMUserTests(TestCase):
|
||||
"userName": uid,
|
||||
},
|
||||
)
|
||||
# Update user
|
||||
user.name = "foo bar"
|
||||
user.save()
|
||||
self.assertEqual(mock.call_count, 4)
|
||||
self.assertEqual(mock.request_history[0].method, "GET")
|
||||
@@ -455,3 +457,85 @@ class SCIMUserTests(TestCase):
|
||||
self.assertIsNotNone(log.attributes["url"])
|
||||
self.assertIsNotNone(log.attributes["body"])
|
||||
self.assertIsNotNone(log.attributes["method"])
|
||||
|
||||
@Mocker()
|
||||
def test_user_create_update_noop(self, mock: Mocker):
|
||||
"""Test user creation and update"""
|
||||
scim_id = generate_id()
|
||||
mock: Mocker
|
||||
mock.get(
|
||||
"https://localhost/ServiceProviderConfig",
|
||||
json={},
|
||||
)
|
||||
mock.post(
|
||||
"https://localhost/Users",
|
||||
json={
|
||||
"id": scim_id,
|
||||
},
|
||||
)
|
||||
mock.put(
|
||||
"https://localhost/Users",
|
||||
json={
|
||||
"id": scim_id,
|
||||
},
|
||||
)
|
||||
uid = generate_id()
|
||||
user = User.objects.create(
|
||||
username=uid,
|
||||
name=f"{uid} {uid}",
|
||||
email=f"{uid}@goauthentik.io",
|
||||
)
|
||||
self.assertEqual(mock.call_count, 2)
|
||||
self.assertEqual(mock.request_history[0].method, "GET")
|
||||
self.assertEqual(mock.request_history[1].method, "POST")
|
||||
body = loads(mock.request_history[1].body)
|
||||
self.assertEqual(
|
||||
body,
|
||||
{
|
||||
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
|
||||
"active": True,
|
||||
"emails": [
|
||||
{
|
||||
"primary": True,
|
||||
"type": "other",
|
||||
"value": f"{uid}@goauthentik.io",
|
||||
}
|
||||
],
|
||||
"displayName": f"{uid} {uid}",
|
||||
"externalId": user.uid,
|
||||
"name": {
|
||||
"familyName": uid,
|
||||
"formatted": f"{uid} {uid}",
|
||||
"givenName": uid,
|
||||
},
|
||||
"userName": uid,
|
||||
},
|
||||
)
|
||||
conn = SCIMProviderUser.objects.filter(user=user).first()
|
||||
conn.attributes = {
|
||||
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
|
||||
"active": True,
|
||||
"emails": [
|
||||
{
|
||||
"primary": True,
|
||||
"type": "other",
|
||||
"value": f"{uid}@goauthentik.io",
|
||||
}
|
||||
],
|
||||
"displayName": f"{uid} {uid}",
|
||||
"externalId": user.uid,
|
||||
"name": {
|
||||
"familyName": uid,
|
||||
"formatted": f"{uid} {uid}",
|
||||
"givenName": uid,
|
||||
},
|
||||
"userName": uid,
|
||||
"id": scim_id,
|
||||
}
|
||||
conn.save()
|
||||
user.save()
|
||||
self.assertEqual(mock.call_count, 3)
|
||||
self.assertEqual(mock.request_history[0].method, "GET")
|
||||
self.assertEqual(mock.request_history[1].method, "POST")
|
||||
self.assertEqual(mock.request_history[2].method, "GET")
|
||||
# No PUT request
|
||||
|
||||
@@ -61,6 +61,22 @@ class SessionMiddleware(UpstreamSessionMiddleware):
|
||||
pass
|
||||
return session_key
|
||||
|
||||
@staticmethod
|
||||
def encode_session(session_key: str, user: User):
|
||||
payload = {
|
||||
"sid": session_key,
|
||||
"iss": "authentik",
|
||||
"sub": "anonymous",
|
||||
"authenticated": user.is_authenticated,
|
||||
"acr": ACR_AUTHENTIK_SESSION,
|
||||
}
|
||||
if user.is_authenticated:
|
||||
payload["sub"] = user.uid
|
||||
value = encode(payload=payload, key=SIGNING_HASH)
|
||||
if settings.TEST:
|
||||
value = session_key
|
||||
return value
|
||||
|
||||
def process_request(self, request: HttpRequest):
|
||||
raw_session = request.COOKIES.get(settings.SESSION_COOKIE_NAME)
|
||||
session_key = SessionMiddleware.decode_session_key(raw_session)
|
||||
@@ -117,21 +133,9 @@ class SessionMiddleware(UpstreamSessionMiddleware):
|
||||
"request completed. The user may have logged "
|
||||
"out in a concurrent request, for example."
|
||||
) from None
|
||||
payload = {
|
||||
"sid": request.session.session_key,
|
||||
"iss": "authentik",
|
||||
"sub": "anonymous",
|
||||
"authenticated": request.user.is_authenticated,
|
||||
"acr": ACR_AUTHENTIK_SESSION,
|
||||
}
|
||||
if request.user.is_authenticated:
|
||||
payload["sub"] = request.user.uid
|
||||
value = encode(payload=payload, key=SIGNING_HASH)
|
||||
if settings.TEST:
|
||||
value = request.session.session_key
|
||||
response.set_cookie(
|
||||
settings.SESSION_COOKIE_NAME,
|
||||
value,
|
||||
SessionMiddleware.encode_session(request.session.session_key, request.user),
|
||||
max_age=max_age,
|
||||
expires=expires,
|
||||
domain=settings.SESSION_COOKIE_DOMAIN,
|
||||
|
||||
@@ -13,7 +13,7 @@ from rest_framework.exceptions import ValidationError
|
||||
from authentik.core.middleware import SESSION_KEY_IMPERSONATE_USER
|
||||
from authentik.core.models import USER_ATTRIBUTE_SOURCES, User, UserSourceConnection, UserTypes
|
||||
from authentik.core.sources.stage import PLAN_CONTEXT_SOURCES_CONNECTION
|
||||
from authentik.events.utils import sanitize_item
|
||||
from authentik.events.utils import sanitize_dict, sanitize_item
|
||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
|
||||
from authentik.flows.stage import StageView
|
||||
from authentik.flows.views.executor import FlowExecutorView
|
||||
@@ -115,7 +115,10 @@ class UserWriteStageView(StageView):
|
||||
continue
|
||||
# For exact attributes match, update the dictionary in place
|
||||
elif key == "attributes":
|
||||
user.attributes.update(value)
|
||||
if isinstance(value, dict):
|
||||
user.attributes.update(sanitize_dict(value))
|
||||
else:
|
||||
raise ValidationError("Attempt to overwrite complete attributes")
|
||||
# If using dot notation, use the correct helper to update the nested value
|
||||
elif key.startswith("attributes.") or key.startswith("attributes_"):
|
||||
UserWriteStageView.write_attribute(user, key, value)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.urls import reverse
|
||||
from django.utils.timezone import now
|
||||
|
||||
from authentik.core.models import (
|
||||
USER_ATTRIBUTE_SOURCES,
|
||||
@@ -128,6 +129,34 @@ class TestUserWriteStage(FlowTestCase):
|
||||
self.assertEqual(user_qs.first().attributes["foo"], "bar")
|
||||
self.assertEqual(user_qs.first().attributes["some_custom_attribute"], "test")
|
||||
|
||||
def test_user_update_complex(self):
|
||||
"""Test update of existing user"""
|
||||
new_password = generate_key()
|
||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
||||
plan.context[PLAN_CONTEXT_PENDING_USER] = User.objects.create(
|
||||
username="unittest", email="test@goauthentik.io"
|
||||
)
|
||||
time = now()
|
||||
plan.context[PLAN_CONTEXT_PROMPT] = {
|
||||
"username": "test-user-new",
|
||||
"password": new_password,
|
||||
"attributes.foo": time,
|
||||
}
|
||||
session = self.client.session
|
||||
session[SESSION_KEY_PLAN] = plan
|
||||
session.save()
|
||||
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertStageRedirects(response, reverse("authentik_core:root-redirect"))
|
||||
user_qs = User.objects.filter(username=plan.context[PLAN_CONTEXT_PROMPT]["username"])
|
||||
self.assertTrue(user_qs.exists())
|
||||
self.assertTrue(user_qs.first().check_password(new_password))
|
||||
self.assertEqual(user_qs.first().attributes["foo"], time.isoformat()[:-6] + "Z")
|
||||
|
||||
def test_user_update_source(self):
|
||||
"""Test update of existing user with a source"""
|
||||
new_password = generate_key()
|
||||
|
||||
@@ -5278,50 +5278,58 @@
|
||||
"authentik_crypto.view_certificatekeypair",
|
||||
"authentik_endpoints.add_connector",
|
||||
"authentik_endpoints.add_device",
|
||||
"authentik_endpoints.add_deviceaccessgroup",
|
||||
"authentik_endpoints.add_deviceconnection",
|
||||
"authentik_endpoints.add_devicefactsnapshot",
|
||||
"authentik_endpoints.add_devicegroup",
|
||||
"authentik_endpoints.add_deviceuserbinding",
|
||||
"authentik_endpoints.add_endpointstage",
|
||||
"authentik_endpoints.change_connector",
|
||||
"authentik_endpoints.change_device",
|
||||
"authentik_endpoints.change_deviceaccessgroup",
|
||||
"authentik_endpoints.change_deviceconnection",
|
||||
"authentik_endpoints.change_devicefactsnapshot",
|
||||
"authentik_endpoints.change_devicegroup",
|
||||
"authentik_endpoints.change_deviceuserbinding",
|
||||
"authentik_endpoints.change_endpointstage",
|
||||
"authentik_endpoints.delete_connector",
|
||||
"authentik_endpoints.delete_device",
|
||||
"authentik_endpoints.delete_deviceaccessgroup",
|
||||
"authentik_endpoints.delete_deviceconnection",
|
||||
"authentik_endpoints.delete_devicefactsnapshot",
|
||||
"authentik_endpoints.delete_devicegroup",
|
||||
"authentik_endpoints.delete_deviceuserbinding",
|
||||
"authentik_endpoints.delete_endpointstage",
|
||||
"authentik_endpoints.view_connector",
|
||||
"authentik_endpoints.view_device",
|
||||
"authentik_endpoints.view_deviceaccessgroup",
|
||||
"authentik_endpoints.view_deviceconnection",
|
||||
"authentik_endpoints.view_devicefactsnapshot",
|
||||
"authentik_endpoints.view_devicegroup",
|
||||
"authentik_endpoints.view_deviceuserbinding",
|
||||
"authentik_endpoints.view_endpointstage",
|
||||
"authentik_endpoints_connectors_agent.add_agentconnector",
|
||||
"authentik_endpoints_connectors_agent.add_agentdeviceconnection",
|
||||
"authentik_endpoints_connectors_agent.add_agentdeviceuserbinding",
|
||||
"authentik_endpoints_connectors_agent.add_applenonce",
|
||||
"authentik_endpoints_connectors_agent.add_deviceauthenticationtoken",
|
||||
"authentik_endpoints_connectors_agent.add_devicetoken",
|
||||
"authentik_endpoints_connectors_agent.add_enrollmenttoken",
|
||||
"authentik_endpoints_connectors_agent.change_agentconnector",
|
||||
"authentik_endpoints_connectors_agent.change_agentdeviceconnection",
|
||||
"authentik_endpoints_connectors_agent.change_agentdeviceuserbinding",
|
||||
"authentik_endpoints_connectors_agent.change_applenonce",
|
||||
"authentik_endpoints_connectors_agent.change_deviceauthenticationtoken",
|
||||
"authentik_endpoints_connectors_agent.change_devicetoken",
|
||||
"authentik_endpoints_connectors_agent.change_enrollmenttoken",
|
||||
"authentik_endpoints_connectors_agent.delete_agentconnector",
|
||||
"authentik_endpoints_connectors_agent.delete_agentdeviceconnection",
|
||||
"authentik_endpoints_connectors_agent.delete_agentdeviceuserbinding",
|
||||
"authentik_endpoints_connectors_agent.delete_applenonce",
|
||||
"authentik_endpoints_connectors_agent.delete_deviceauthenticationtoken",
|
||||
"authentik_endpoints_connectors_agent.delete_devicetoken",
|
||||
"authentik_endpoints_connectors_agent.delete_enrollmenttoken",
|
||||
"authentik_endpoints_connectors_agent.view_agentconnector",
|
||||
"authentik_endpoints_connectors_agent.view_agentdeviceconnection",
|
||||
"authentik_endpoints_connectors_agent.view_agentdeviceuserbinding",
|
||||
"authentik_endpoints_connectors_agent.view_applenonce",
|
||||
"authentik_endpoints_connectors_agent.view_deviceauthenticationtoken",
|
||||
"authentik_endpoints_connectors_agent.view_devicetoken",
|
||||
"authentik_endpoints_connectors_agent.view_enrollment_token_key",
|
||||
"authentik_endpoints_connectors_agent.view_enrollmenttoken",
|
||||
@@ -5973,6 +5981,25 @@
|
||||
"minLength": 1,
|
||||
"title": "Snapshot expiry"
|
||||
},
|
||||
"auth_session_duration": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"title": "Auth session duration"
|
||||
},
|
||||
"auth_terminate_session_on_expiry": {
|
||||
"type": "boolean",
|
||||
"title": "Auth terminate session on expiry"
|
||||
},
|
||||
"refresh_interval": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"title": "Refresh interval"
|
||||
},
|
||||
"authorization_flow": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"title": "Authorization flow"
|
||||
},
|
||||
"nss_uid_offset": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
@@ -5985,24 +6012,17 @@
|
||||
"maximum": 2147483647,
|
||||
"title": "Nss gid offset"
|
||||
},
|
||||
"auth_terminate_session_on_expiry": {
|
||||
"type": "boolean",
|
||||
"title": "Auth terminate session on expiry"
|
||||
},
|
||||
"refresh_interval": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"title": "Refresh interval"
|
||||
},
|
||||
"authentication_flow": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"title": "Authentication flow"
|
||||
},
|
||||
"challenge_key": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"title": "Challenge key"
|
||||
},
|
||||
"jwt_federation_providers": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer"
|
||||
},
|
||||
"title": "Jwt federation providers"
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
@@ -6267,9 +6287,7 @@
|
||||
},
|
||||
"slug": {
|
||||
"type": "string",
|
||||
"maxLength": 50,
|
||||
"minLength": 1,
|
||||
"pattern": "^[-a-zA-Z0-9_]+$",
|
||||
"title": "Slug",
|
||||
"description": "Visible in the URL."
|
||||
},
|
||||
@@ -10558,50 +10576,58 @@
|
||||
"authentik_crypto.view_certificatekeypair",
|
||||
"authentik_endpoints.add_connector",
|
||||
"authentik_endpoints.add_device",
|
||||
"authentik_endpoints.add_deviceaccessgroup",
|
||||
"authentik_endpoints.add_deviceconnection",
|
||||
"authentik_endpoints.add_devicefactsnapshot",
|
||||
"authentik_endpoints.add_devicegroup",
|
||||
"authentik_endpoints.add_deviceuserbinding",
|
||||
"authentik_endpoints.add_endpointstage",
|
||||
"authentik_endpoints.change_connector",
|
||||
"authentik_endpoints.change_device",
|
||||
"authentik_endpoints.change_deviceaccessgroup",
|
||||
"authentik_endpoints.change_deviceconnection",
|
||||
"authentik_endpoints.change_devicefactsnapshot",
|
||||
"authentik_endpoints.change_devicegroup",
|
||||
"authentik_endpoints.change_deviceuserbinding",
|
||||
"authentik_endpoints.change_endpointstage",
|
||||
"authentik_endpoints.delete_connector",
|
||||
"authentik_endpoints.delete_device",
|
||||
"authentik_endpoints.delete_deviceaccessgroup",
|
||||
"authentik_endpoints.delete_deviceconnection",
|
||||
"authentik_endpoints.delete_devicefactsnapshot",
|
||||
"authentik_endpoints.delete_devicegroup",
|
||||
"authentik_endpoints.delete_deviceuserbinding",
|
||||
"authentik_endpoints.delete_endpointstage",
|
||||
"authentik_endpoints.view_connector",
|
||||
"authentik_endpoints.view_device",
|
||||
"authentik_endpoints.view_deviceaccessgroup",
|
||||
"authentik_endpoints.view_deviceconnection",
|
||||
"authentik_endpoints.view_devicefactsnapshot",
|
||||
"authentik_endpoints.view_devicegroup",
|
||||
"authentik_endpoints.view_deviceuserbinding",
|
||||
"authentik_endpoints.view_endpointstage",
|
||||
"authentik_endpoints_connectors_agent.add_agentconnector",
|
||||
"authentik_endpoints_connectors_agent.add_agentdeviceconnection",
|
||||
"authentik_endpoints_connectors_agent.add_agentdeviceuserbinding",
|
||||
"authentik_endpoints_connectors_agent.add_applenonce",
|
||||
"authentik_endpoints_connectors_agent.add_deviceauthenticationtoken",
|
||||
"authentik_endpoints_connectors_agent.add_devicetoken",
|
||||
"authentik_endpoints_connectors_agent.add_enrollmenttoken",
|
||||
"authentik_endpoints_connectors_agent.change_agentconnector",
|
||||
"authentik_endpoints_connectors_agent.change_agentdeviceconnection",
|
||||
"authentik_endpoints_connectors_agent.change_agentdeviceuserbinding",
|
||||
"authentik_endpoints_connectors_agent.change_applenonce",
|
||||
"authentik_endpoints_connectors_agent.change_deviceauthenticationtoken",
|
||||
"authentik_endpoints_connectors_agent.change_devicetoken",
|
||||
"authentik_endpoints_connectors_agent.change_enrollmenttoken",
|
||||
"authentik_endpoints_connectors_agent.delete_agentconnector",
|
||||
"authentik_endpoints_connectors_agent.delete_agentdeviceconnection",
|
||||
"authentik_endpoints_connectors_agent.delete_agentdeviceuserbinding",
|
||||
"authentik_endpoints_connectors_agent.delete_applenonce",
|
||||
"authentik_endpoints_connectors_agent.delete_deviceauthenticationtoken",
|
||||
"authentik_endpoints_connectors_agent.delete_devicetoken",
|
||||
"authentik_endpoints_connectors_agent.delete_enrollmenttoken",
|
||||
"authentik_endpoints_connectors_agent.view_agentconnector",
|
||||
"authentik_endpoints_connectors_agent.view_agentdeviceconnection",
|
||||
"authentik_endpoints_connectors_agent.view_agentdeviceuserbinding",
|
||||
"authentik_endpoints_connectors_agent.view_applenonce",
|
||||
"authentik_endpoints_connectors_agent.view_deviceauthenticationtoken",
|
||||
"authentik_endpoints_connectors_agent.view_devicetoken",
|
||||
"authentik_endpoints_connectors_agent.view_enrollment_token_key",
|
||||
"authentik_endpoints_connectors_agent.view_enrollmenttoken",
|
||||
|
||||
4
go.mod
4
go.mod
@@ -9,7 +9,7 @@ require (
|
||||
beryju.io/radius-eap v0.1.0
|
||||
github.com/avast/retry-go/v4 v4.7.0
|
||||
github.com/coreos/go-oidc/v3 v3.17.0
|
||||
github.com/getsentry/sentry-go v0.39.0
|
||||
github.com/getsentry/sentry-go v0.40.0
|
||||
github.com/go-http-utils/etag v0.0.0-20161124023236-513ea8f21eb1
|
||||
github.com/go-ldap/ldap/v3 v3.4.12
|
||||
github.com/go-openapi/runtime v0.29.2
|
||||
@@ -32,7 +32,7 @@ require (
|
||||
github.com/spf13/cobra v1.10.1
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/wwt/guac v1.3.2
|
||||
goauthentik.io/api/v3 v3.2025120.7
|
||||
goauthentik.io/api/v3 v3.2025120.11
|
||||
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
|
||||
golang.org/x/oauth2 v0.33.0
|
||||
golang.org/x/sync v0.18.0
|
||||
|
||||
8
go.sum
8
go.sum
@@ -20,8 +20,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
|
||||
github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/getsentry/sentry-go v0.39.0 h1:uhnexj8PNCyCve37GSqxXOeXHh4cJNLNNB4w70Jtgo0=
|
||||
github.com/getsentry/sentry-go v0.39.0/go.mod h1:eRXCoh3uvmjQLY6qu63BjUZnaBu5L5WhMV1RwYO8W5s=
|
||||
github.com/getsentry/sentry-go v0.40.0 h1:VTJMN9zbTvqDqPwheRVLcp0qcUcM+8eFivvGocAaSbo=
|
||||
github.com/getsentry/sentry-go v0.40.0/go.mod h1:eRXCoh3uvmjQLY6qu63BjUZnaBu5L5WhMV1RwYO8W5s=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
|
||||
@@ -214,8 +214,8 @@ go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
|
||||
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
goauthentik.io/api/v3 v3.2025120.7 h1:kcMcIm1l0ndbmEu4JlIHL2pscmdN2x4kAn0t8Zd+RNI=
|
||||
goauthentik.io/api/v3 v3.2025120.7/go.mod h1:82lqAz4jxzl6Cg0YDbhNtvvTG2rm6605ZhdJFnbbsl8=
|
||||
goauthentik.io/api/v3 v3.2025120.11 h1:qKKIj6FzWMG5x3suQyKJdD4ziIlqtp6Lq8SCjjQAMf0=
|
||||
goauthentik.io/api/v3 v3.2025120.11/go.mod h1:82lqAz4jxzl6Cg0YDbhNtvvTG2rm6605ZhdJFnbbsl8=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
|
||||
@@ -31,7 +31,7 @@ RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \
|
||||
go build -o /go/ldap ./cmd/ldap
|
||||
|
||||
# Stage 2: Run
|
||||
FROM ghcr.io/goauthentik/fips-debian:trixie-slim-fips@sha256:ac4c80b43351a3a10c0307d191fe6a6386359bc36e2aa3ccb1b23abd1f0a9e4f
|
||||
FROM ghcr.io/goauthentik/fips-debian:trixie-slim-fips@sha256:c718f608885a76a8940700b0c9feab75b1c967e1c291adf52dcd3ad6f6a86a66
|
||||
|
||||
ARG VERSION
|
||||
ARG GIT_BUILD_HASH
|
||||
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -27,27 +27,28 @@
|
||||
# Stefan Werner, 2024
|
||||
# Jonas, 2025
|
||||
# Niklas Kroese, 2025
|
||||
# 97cce0ae0cad2a2cc552d3165d04643e_de3d740, 2025
|
||||
# Dominic Wagner <mail@dominic-wagner.de>, 2025
|
||||
# Till-Frederik Riechard, 2025
|
||||
# Alexander Mnich, 2025
|
||||
# Ben, 2025
|
||||
# datenschmutz, 2025
|
||||
# Ulrich Stark, 2025
|
||||
# Benjamin Böhmke, 2025
|
||||
# 97cce0ae0cad2a2cc552d3165d04643e_de3d740, 2025
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-10-20 00:11+0000\n"
|
||||
"POT-Creation-Date: 2025-12-01 15:07+0000\n"
|
||||
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
|
||||
"Last-Translator: Benjamin Böhmke, 2025\n"
|
||||
"Last-Translator: 97cce0ae0cad2a2cc552d3165d04643e_de3d740, 2025\n"
|
||||
"Language-Team: German (https://app.transifex.com/authentik/teams/119923/de/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Language: de\n"
|
||||
"Language: de_DE\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: authentik/admin/models.py
|
||||
@@ -419,6 +420,12 @@ msgstr "Quellname"
|
||||
msgid "Internal source name, used in URLs."
|
||||
msgstr "Interner Quellname, genutzt für URLs"
|
||||
|
||||
#: authentik/core/models.py
|
||||
msgid ""
|
||||
"When enabled, this source will be displayed as a prominent button on the "
|
||||
"login page, instead of a small icon."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/models.py
|
||||
msgid "Flow to use when authenticating existing users."
|
||||
msgstr "Flow der zur Authorisierung bereits ersteller Nutzer verwendet wird"
|
||||
@@ -451,7 +458,7 @@ msgstr "Token"
|
||||
msgid "Tokens"
|
||||
msgstr "Tokens"
|
||||
|
||||
#: authentik/core/models.py
|
||||
#: authentik/core/models.py authentik/endpoints/connectors/agent/models.py
|
||||
msgid "View token's key"
|
||||
msgstr "Schlüssel des Tokens anzeigen"
|
||||
|
||||
@@ -524,6 +531,7 @@ msgid "Remove temporary users created by SAML Sources."
|
||||
msgstr "Temporäre Benutzer entfernen, die von SAML-Sources erstellt wurden."
|
||||
|
||||
#: authentik/core/templates/if/error.html
|
||||
#: authentik/policies/templates/policies/denied.html
|
||||
msgid "Go home"
|
||||
msgstr "Zur Startseite"
|
||||
|
||||
@@ -555,6 +563,26 @@ msgstr "RSA"
|
||||
msgid "ecdsa"
|
||||
msgstr "ECDSA"
|
||||
|
||||
#: authentik/crypto/models.py
|
||||
msgid "RSA"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/crypto/models.py
|
||||
msgid "Elliptic Curve"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/crypto/models.py
|
||||
msgid "DSA"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/crypto/models.py
|
||||
msgid "Ed25519"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/crypto/models.py
|
||||
msgid "Ed448"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/crypto/models.py
|
||||
msgid "PEM-encoded Certificate data"
|
||||
msgstr "PEM-verschlüsselte Zertifikatsdaten"
|
||||
@@ -579,6 +607,104 @@ msgstr "Zertifikat-Schlüsselpaare"
|
||||
msgid "Discover, import and update certificates from the filesystem."
|
||||
msgstr "Zertifikate vom Dateisystem entdecken, importieren und aktualisieren."
|
||||
|
||||
#: authentik/endpoints/connectors/agent/api/connectors.py
|
||||
msgid "Selected platform not supported"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/api/connectors.py
|
||||
msgid "Token is expired"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/api/connectors.py
|
||||
msgid "Invalid token for connector"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
msgid "Agent Connector"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
msgid "Agent Connectors"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
#: authentik/providers/oauth2/models.py
|
||||
msgid "Device Token"
|
||||
msgstr "Geräte-Token"
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
#: authentik/providers/oauth2/models.py
|
||||
msgid "Device Tokens"
|
||||
msgstr "Geräte-Token"
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
msgid "Enrollment Token"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
msgid "Enrollment Tokens"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
msgid "Device authentication token"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
msgid "Device authentication tokens"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
msgid "Apple Nonce"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
msgid "Apple Nonces"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device"
|
||||
msgstr "Gerät"
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Devices"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device User binding"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device User bindings"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device connection"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device connections"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device fact snapshot"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device fact snapshots"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device access group"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device access groups"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/tasks.py
|
||||
msgid "Sync endpoints."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/enterprise/api.py
|
||||
msgid "Enterprise is required to create/update this object."
|
||||
msgstr ""
|
||||
@@ -1250,10 +1376,22 @@ msgstr "Flow-Token"
|
||||
msgid "Flow Tokens"
|
||||
msgstr "Flow-Tokens"
|
||||
|
||||
#: authentik/flows/templates/if/flow.html
|
||||
msgid "Site footer"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/flows/views/executor.py
|
||||
msgid "Invalid next URL"
|
||||
msgstr "Ungültige nächste URL"
|
||||
|
||||
#: authentik/lib/sync/outgoing/models.py
|
||||
msgid "Controls the number of objects synced in a single task"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/lib/sync/outgoing/models.py
|
||||
msgid "Timeout for synchronization of a single page"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/lib/sync/outgoing/models.py
|
||||
msgid ""
|
||||
"When enabled, provider will not modify or create objects in the remote "
|
||||
@@ -1657,6 +1795,10 @@ msgstr "Avatar des Benutzers"
|
||||
msgid "Not you?"
|
||||
msgstr "Nicht Sie?"
|
||||
|
||||
#: authentik/policies/templates/policies/denied.html
|
||||
msgid "Error"
|
||||
msgstr "Fehler"
|
||||
|
||||
#: authentik/policies/templates/policies/denied.html
|
||||
msgid "Request has been denied."
|
||||
msgstr "Anfrage wurde verweigert"
|
||||
@@ -2040,14 +2182,6 @@ msgstr "OAuth2-Aktualisierungs-Token"
|
||||
msgid "OAuth2 Refresh Tokens"
|
||||
msgstr "OAuth2-Aktualisierungs-Token"
|
||||
|
||||
#: authentik/providers/oauth2/models.py
|
||||
msgid "Device Token"
|
||||
msgstr "Geräte-Token"
|
||||
|
||||
#: authentik/providers/oauth2/models.py
|
||||
msgid "Device Tokens"
|
||||
msgstr "Geräte-Token"
|
||||
|
||||
#: authentik/providers/oauth2/tasks.py
|
||||
msgid "Send a back-channel logout request to the registered client"
|
||||
msgstr "Eine Back-Channel-Logout-Anfrage an den registrierten Client senden."
|
||||
@@ -2062,7 +2196,7 @@ msgstr ""
|
||||
#: authentik/providers/saml/views/flows.py
|
||||
#, python-brace-format
|
||||
msgid "Redirecting to {app}..."
|
||||
msgstr "Umleitung zu {app}..."
|
||||
msgstr "Weiterleitung zu {app}..."
|
||||
|
||||
#: authentik/providers/oauth2/views/device_init.py
|
||||
msgid "Invalid code"
|
||||
@@ -2285,18 +2419,6 @@ msgstr "Der Import von Metadaten ist fehlgeschlagen: {messages}"
|
||||
msgid "ACS URL"
|
||||
msgstr "ACS URL"
|
||||
|
||||
#: authentik/providers/saml/models.py
|
||||
msgid ""
|
||||
"Value of the audience restriction field of the assertion. When left empty, "
|
||||
"no audience restriction will be added."
|
||||
msgstr ""
|
||||
"Wert des Feldes für die Zielgruppenbeschränkung in der Prüfung. Bleibt das "
|
||||
"Feld leer, wird keine Zielgruppenbeschränkung hinzugefügt."
|
||||
|
||||
#: authentik/providers/saml/models.py
|
||||
msgid "Also known as EntityID"
|
||||
msgstr "Auch bekannt als EntityID"
|
||||
|
||||
#: authentik/providers/saml/models.py
|
||||
msgid "Service Provider Binding"
|
||||
msgstr "Service Provider zuordung"
|
||||
@@ -2309,6 +2431,18 @@ msgstr ""
|
||||
"Damit wird festgelegt, wie Authentik die Antwort an den Dienstanbieter "
|
||||
"zurücksendet."
|
||||
|
||||
#: authentik/providers/saml/models.py
|
||||
msgid ""
|
||||
"Value of the audience restriction field of the assertion. When left empty, "
|
||||
"no audience restriction will be added."
|
||||
msgstr ""
|
||||
"Wert des Feldes für die Zielgruppenbeschränkung in der Prüfung. Bleibt das "
|
||||
"Feld leer, wird keine Zielgruppenbeschränkung hinzugefügt."
|
||||
|
||||
#: authentik/providers/saml/models.py
|
||||
msgid "Also known as EntityID"
|
||||
msgstr "Auch bekannt als EntityID"
|
||||
|
||||
#: authentik/providers/saml/models.py
|
||||
msgid "SLS URL"
|
||||
msgstr ""
|
||||
Binary file not shown.
@@ -8,15 +8,14 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-11-25 00:09+0000\n"
|
||||
"POT-Creation-Date: 2025-12-01 17:05+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: \n"
|
||||
"Language: en\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: authentik/admin/models.py
|
||||
msgid "Version history"
|
||||
@@ -348,6 +347,12 @@ msgstr ""
|
||||
msgid "Internal source name, used in URLs."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/models.py
|
||||
msgid ""
|
||||
"When enabled, this source will be displayed as a prominent button on the "
|
||||
"login page, instead of a small icon."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/models.py
|
||||
msgid "Flow to use when authenticating existing users."
|
||||
msgstr ""
|
||||
@@ -540,6 +545,16 @@ msgstr ""
|
||||
msgid "Agent Connectors"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
#: authentik/providers/oauth2/models.py
|
||||
msgid "Device Token"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
#: authentik/providers/oauth2/models.py
|
||||
msgid "Device Tokens"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
msgid "Enrollment Token"
|
||||
msgstr ""
|
||||
@@ -548,6 +563,62 @@ msgstr ""
|
||||
msgid "Enrollment Tokens"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
msgid "Device authentication token"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
msgid "Device authentication tokens"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
msgid "Apple Nonce"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
msgid "Apple Nonces"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Devices"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device User binding"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device User bindings"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device connection"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device connections"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device fact snapshot"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device fact snapshots"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device access group"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device access groups"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/tasks.py
|
||||
msgid "Sync endpoints."
|
||||
msgstr ""
|
||||
@@ -1873,14 +1944,6 @@ msgstr ""
|
||||
msgid "OAuth2 Refresh Tokens"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/oauth2/models.py
|
||||
msgid "Device Token"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/oauth2/models.py
|
||||
msgid "Device Tokens"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/oauth2/tasks.py
|
||||
msgid "Send a back-channel logout request to the registered client"
|
||||
msgstr ""
|
||||
|
||||
Binary file not shown.
BIN
locale/es_ES/LC_MESSAGES/django.mo
Normal file
BIN
locale/es_ES/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
@@ -16,14 +16,14 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-10-20 00:11+0000\n"
|
||||
"POT-Creation-Date: 2025-12-01 15:07+0000\n"
|
||||
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
|
||||
"Last-Translator: Jens L. <jens@goauthentik.io>, 2025\n"
|
||||
"Language-Team: Spanish (https://app.transifex.com/authentik/teams/119923/es/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Language: es\n"
|
||||
"Language: es_ES\n"
|
||||
"Plural-Forms: nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\n"
|
||||
|
||||
#: authentik/admin/models.py
|
||||
@@ -394,6 +394,12 @@ msgstr "Nombre de visualización de la fuente."
|
||||
msgid "Internal source name, used in URLs."
|
||||
msgstr "Nombre de origen interno, utilizado en las URL."
|
||||
|
||||
#: authentik/core/models.py
|
||||
msgid ""
|
||||
"When enabled, this source will be displayed as a prominent button on the "
|
||||
"login page, instead of a small icon."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/models.py
|
||||
msgid "Flow to use when authenticating existing users."
|
||||
msgstr "Flujo que se utilizará al autenticar a los usuarios existentes."
|
||||
@@ -426,7 +432,7 @@ msgstr "Token"
|
||||
msgid "Tokens"
|
||||
msgstr "Tokens"
|
||||
|
||||
#: authentik/core/models.py
|
||||
#: authentik/core/models.py authentik/endpoints/connectors/agent/models.py
|
||||
msgid "View token's key"
|
||||
msgstr "Ver llave del token"
|
||||
|
||||
@@ -498,6 +504,7 @@ msgid "Remove temporary users created by SAML Sources."
|
||||
msgstr "Eliminar usuarios temporales creados por SAML Sources."
|
||||
|
||||
#: authentik/core/templates/if/error.html
|
||||
#: authentik/policies/templates/policies/denied.html
|
||||
msgid "Go home"
|
||||
msgstr "Ir al inicio"
|
||||
|
||||
@@ -529,6 +536,26 @@ msgstr "rsa"
|
||||
msgid "ecdsa"
|
||||
msgstr "ecdsa"
|
||||
|
||||
#: authentik/crypto/models.py
|
||||
msgid "RSA"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/crypto/models.py
|
||||
msgid "Elliptic Curve"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/crypto/models.py
|
||||
msgid "DSA"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/crypto/models.py
|
||||
msgid "Ed25519"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/crypto/models.py
|
||||
msgid "Ed448"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/crypto/models.py
|
||||
msgid "PEM-encoded Certificate data"
|
||||
msgstr "Datos de certificados codificados en PEM"
|
||||
@@ -554,6 +581,104 @@ msgid "Discover, import and update certificates from the filesystem."
|
||||
msgstr ""
|
||||
"Descubra, importe y actualice certificados desde el sistema de archivos."
|
||||
|
||||
#: authentik/endpoints/connectors/agent/api/connectors.py
|
||||
msgid "Selected platform not supported"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/api/connectors.py
|
||||
msgid "Token is expired"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/api/connectors.py
|
||||
msgid "Invalid token for connector"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
msgid "Agent Connector"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
msgid "Agent Connectors"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
#: authentik/providers/oauth2/models.py
|
||||
msgid "Device Token"
|
||||
msgstr "Token de Dispositivo"
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
#: authentik/providers/oauth2/models.py
|
||||
msgid "Device Tokens"
|
||||
msgstr "Tokens de Dispositivo"
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
msgid "Enrollment Token"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
msgid "Enrollment Tokens"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
msgid "Device authentication token"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
msgid "Device authentication tokens"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
msgid "Apple Nonce"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
msgid "Apple Nonces"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device"
|
||||
msgstr "Dispositivo"
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Devices"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device User binding"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device User bindings"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device connection"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device connections"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device fact snapshot"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device fact snapshots"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device access group"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device access groups"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/tasks.py
|
||||
msgid "Sync endpoints."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/enterprise/api.py
|
||||
msgid "Enterprise is required to create/update this object."
|
||||
msgstr "Se requiere de Enterprise para crear/actualizar este objeto."
|
||||
@@ -1211,10 +1336,22 @@ msgstr "Token de flujo"
|
||||
msgid "Flow Tokens"
|
||||
msgstr "Tokens de flujo"
|
||||
|
||||
#: authentik/flows/templates/if/flow.html
|
||||
msgid "Site footer"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/flows/views/executor.py
|
||||
msgid "Invalid next URL"
|
||||
msgstr "Siguiente URL invalida"
|
||||
|
||||
#: authentik/lib/sync/outgoing/models.py
|
||||
msgid "Controls the number of objects synced in a single task"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/lib/sync/outgoing/models.py
|
||||
msgid "Timeout for synchronization of a single page"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/lib/sync/outgoing/models.py
|
||||
msgid ""
|
||||
"When enabled, provider will not modify or create objects in the remote "
|
||||
@@ -1615,6 +1752,10 @@ msgstr "Avatar del usuario"
|
||||
msgid "Not you?"
|
||||
msgstr "¿No eres tú?"
|
||||
|
||||
#: authentik/policies/templates/policies/denied.html
|
||||
msgid "Error"
|
||||
msgstr "Error"
|
||||
|
||||
#: authentik/policies/templates/policies/denied.html
|
||||
msgid "Request has been denied."
|
||||
msgstr "Se ha denegado la solicitud."
|
||||
@@ -1995,14 +2136,6 @@ msgstr "Token de Actualización OAuth2"
|
||||
msgid "OAuth2 Refresh Tokens"
|
||||
msgstr "Tokens de Actualización OAuth2"
|
||||
|
||||
#: authentik/providers/oauth2/models.py
|
||||
msgid "Device Token"
|
||||
msgstr "Token de Dispositivo"
|
||||
|
||||
#: authentik/providers/oauth2/models.py
|
||||
msgid "Device Tokens"
|
||||
msgstr "Tokens de Dispositivo"
|
||||
|
||||
#: authentik/providers/oauth2/tasks.py
|
||||
msgid "Send a back-channel logout request to the registered client"
|
||||
msgstr ""
|
||||
@@ -2245,6 +2378,17 @@ msgstr "No se pudieron importar los Metadatos: {messages}"
|
||||
msgid "ACS URL"
|
||||
msgstr "URL"
|
||||
|
||||
#: authentik/providers/saml/models.py
|
||||
msgid "Service Provider Binding"
|
||||
msgstr "Vinculación de Proveedor de Servicio"
|
||||
|
||||
#: authentik/providers/saml/models.py
|
||||
msgid ""
|
||||
"This determines how authentik sends the response back to the Service "
|
||||
"Provider."
|
||||
msgstr ""
|
||||
"Esto determina cómo authentik envía la respuesta al Proveedor de Servicios."
|
||||
|
||||
#: authentik/providers/saml/models.py
|
||||
msgid ""
|
||||
"Value of the audience restriction field of the assertion. When left empty, "
|
||||
@@ -2257,17 +2401,6 @@ msgstr ""
|
||||
msgid "Also known as EntityID"
|
||||
msgstr "También conocido como EntityID"
|
||||
|
||||
#: authentik/providers/saml/models.py
|
||||
msgid "Service Provider Binding"
|
||||
msgstr "Vinculación de Proveedor de Servicio"
|
||||
|
||||
#: authentik/providers/saml/models.py
|
||||
msgid ""
|
||||
"This determines how authentik sends the response back to the Service "
|
||||
"Provider."
|
||||
msgstr ""
|
||||
"Esto determina cómo authentik envía la respuesta al Proveedor de Servicios."
|
||||
|
||||
#: authentik/providers/saml/models.py
|
||||
msgid "SLS URL"
|
||||
msgstr ""
|
||||
@@ -15,14 +15,14 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-11-13 03:19+0000\n"
|
||||
"POT-Creation-Date: 2025-12-01 15:07+0000\n"
|
||||
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
|
||||
"Last-Translator: Skyler Mäntysaari, 2025\n"
|
||||
"Language-Team: Finnish (https://app.transifex.com/authentik/teams/119923/fi/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Language: fi\n"
|
||||
"Language: fi_FI\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: authentik/admin/models.py
|
||||
@@ -382,6 +382,12 @@ msgstr "Lähteen näytettävä nimi."
|
||||
msgid "Internal source name, used in URLs."
|
||||
msgstr "Lähteen sisäinen nimi, käytetään URLeissa."
|
||||
|
||||
#: authentik/core/models.py
|
||||
msgid ""
|
||||
"When enabled, this source will be displayed as a prominent button on the "
|
||||
"login page, instead of a small icon."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/models.py
|
||||
msgid "Flow to use when authenticating existing users."
|
||||
msgstr "Prosessi, jota käytetään kun todennetaan olemassa olevia käyttäjiä."
|
||||
@@ -414,7 +420,7 @@ msgstr "Tunniste"
|
||||
msgid "Tokens"
|
||||
msgstr "Tunnisteet"
|
||||
|
||||
#: authentik/core/models.py
|
||||
#: authentik/core/models.py authentik/endpoints/connectors/agent/models.py
|
||||
msgid "View token's key"
|
||||
msgstr "Näytä tunnisteen avain"
|
||||
|
||||
@@ -562,6 +568,104 @@ msgstr "Sertifikaatti-avainparit"
|
||||
msgid "Discover, import and update certificates from the filesystem."
|
||||
msgstr "Havaitse, tuo ja päivitä sertifikaatteja levyjärjestelmästä."
|
||||
|
||||
#: authentik/endpoints/connectors/agent/api/connectors.py
|
||||
msgid "Selected platform not supported"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/api/connectors.py
|
||||
msgid "Token is expired"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/api/connectors.py
|
||||
msgid "Invalid token for connector"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
msgid "Agent Connector"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
msgid "Agent Connectors"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
#: authentik/providers/oauth2/models.py
|
||||
msgid "Device Token"
|
||||
msgstr "Laitetunniste"
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
#: authentik/providers/oauth2/models.py
|
||||
msgid "Device Tokens"
|
||||
msgstr "Laitetunnisteet"
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
msgid "Enrollment Token"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
msgid "Enrollment Tokens"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
msgid "Device authentication token"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
msgid "Device authentication tokens"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
msgid "Apple Nonce"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
msgid "Apple Nonces"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device"
|
||||
msgstr "Laite"
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Devices"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device User binding"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device User bindings"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device connection"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device connections"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device fact snapshot"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device fact snapshots"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device access group"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device access groups"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/tasks.py
|
||||
msgid "Sync endpoints."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/enterprise/api.py
|
||||
msgid "Enterprise is required to create/update this object."
|
||||
msgstr "Tämän objektin luontiin/päivittämiseen tarvitaan Enterprise-versiota."
|
||||
@@ -1215,6 +1319,10 @@ msgstr "Prosessin tunniste"
|
||||
msgid "Flow Tokens"
|
||||
msgstr "Prosessin tunnisteet"
|
||||
|
||||
#: authentik/flows/templates/if/flow.html
|
||||
msgid "Site footer"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/flows/views/executor.py
|
||||
msgid "Invalid next URL"
|
||||
msgstr "Virheellinen seuraava URL"
|
||||
@@ -2012,14 +2120,6 @@ msgstr "OAuth2-päivitystunnus"
|
||||
msgid "OAuth2 Refresh Tokens"
|
||||
msgstr "OAuth2-päivitystunnukset"
|
||||
|
||||
#: authentik/providers/oauth2/models.py
|
||||
msgid "Device Token"
|
||||
msgstr "Laitetunniste"
|
||||
|
||||
#: authentik/providers/oauth2/models.py
|
||||
msgid "Device Tokens"
|
||||
msgstr "Laitetunnisteet"
|
||||
|
||||
#: authentik/providers/oauth2/tasks.py
|
||||
msgid "Send a back-channel logout request to the registered client"
|
||||
msgstr ""
|
||||
@@ -9,24 +9,24 @@
|
||||
# Kyllian Delaye-Maillot, 2023
|
||||
# Manuel Viens, 2023
|
||||
# Mordecai, 2023
|
||||
# Charles Leclerc, 2025
|
||||
# Tina, 2025
|
||||
# nerdinator <florian.dupret@gmail.com>, 2025
|
||||
# Marc Schmitt, 2025
|
||||
# Charles Leclerc, 2025
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-10-20 00:11+0000\n"
|
||||
"POT-Creation-Date: 2025-12-01 15:07+0000\n"
|
||||
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
|
||||
"Last-Translator: Marc Schmitt, 2025\n"
|
||||
"Last-Translator: Charles Leclerc, 2025\n"
|
||||
"Language-Team: French (https://app.transifex.com/authentik/teams/119923/fr/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Language: fr\n"
|
||||
"Language: fr_FR\n"
|
||||
"Plural-Forms: nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\n"
|
||||
|
||||
#: authentik/admin/models.py
|
||||
@@ -397,6 +397,12 @@ msgstr "Nom d'affichage de la source."
|
||||
msgid "Internal source name, used in URLs."
|
||||
msgstr "Nom interne de la source, utilisé dans les URLs."
|
||||
|
||||
#: authentik/core/models.py
|
||||
msgid ""
|
||||
"When enabled, this source will be displayed as a prominent button on the "
|
||||
"login page, instead of a small icon."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/models.py
|
||||
msgid "Flow to use when authenticating existing users."
|
||||
msgstr "Flux à utiliser pour authentifier les utilisateurs existants."
|
||||
@@ -429,7 +435,7 @@ msgstr "Jeton"
|
||||
msgid "Tokens"
|
||||
msgstr "Jetons"
|
||||
|
||||
#: authentik/core/models.py
|
||||
#: authentik/core/models.py authentik/endpoints/connectors/agent/models.py
|
||||
msgid "View token's key"
|
||||
msgstr "Voir la clé du jeton"
|
||||
|
||||
@@ -501,6 +507,7 @@ msgid "Remove temporary users created by SAML Sources."
|
||||
msgstr "Supprime les utilisateurs temporaires créés par les sources SAML."
|
||||
|
||||
#: authentik/core/templates/if/error.html
|
||||
#: authentik/policies/templates/policies/denied.html
|
||||
msgid "Go home"
|
||||
msgstr "Retourner à l'accueil"
|
||||
|
||||
@@ -532,6 +539,26 @@ msgstr "rsa"
|
||||
msgid "ecdsa"
|
||||
msgstr "ecdsa"
|
||||
|
||||
#: authentik/crypto/models.py
|
||||
msgid "RSA"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/crypto/models.py
|
||||
msgid "Elliptic Curve"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/crypto/models.py
|
||||
msgid "DSA"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/crypto/models.py
|
||||
msgid "Ed25519"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/crypto/models.py
|
||||
msgid "Ed448"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/crypto/models.py
|
||||
msgid "PEM-encoded Certificate data"
|
||||
msgstr "Données du certificat au format PEM"
|
||||
@@ -558,6 +585,104 @@ msgstr ""
|
||||
"Découvre, importe et met à jour les certificats depuis le système de "
|
||||
"fichiers."
|
||||
|
||||
#: authentik/endpoints/connectors/agent/api/connectors.py
|
||||
msgid "Selected platform not supported"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/api/connectors.py
|
||||
msgid "Token is expired"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/api/connectors.py
|
||||
msgid "Invalid token for connector"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
msgid "Agent Connector"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
msgid "Agent Connectors"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
#: authentik/providers/oauth2/models.py
|
||||
msgid "Device Token"
|
||||
msgstr "Jeton d'équipement"
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
#: authentik/providers/oauth2/models.py
|
||||
msgid "Device Tokens"
|
||||
msgstr "Jetons d'équipement"
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
msgid "Enrollment Token"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
msgid "Enrollment Tokens"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
msgid "Device authentication token"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
msgid "Device authentication tokens"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
msgid "Apple Nonce"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
msgid "Apple Nonces"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device"
|
||||
msgstr "Équipement"
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Devices"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device User binding"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device User bindings"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device connection"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device connections"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device fact snapshot"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device fact snapshots"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device access group"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device access groups"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/tasks.py
|
||||
msgid "Sync endpoints."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/enterprise/api.py
|
||||
msgid "Enterprise is required to create/update this object."
|
||||
msgstr "Entreprise est requis pour créer/mettre à jour cet objet."
|
||||
@@ -1225,10 +1350,22 @@ msgstr "Jeton du flux"
|
||||
msgid "Flow Tokens"
|
||||
msgstr "Jetons du flux"
|
||||
|
||||
#: authentik/flows/templates/if/flow.html
|
||||
msgid "Site footer"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/flows/views/executor.py
|
||||
msgid "Invalid next URL"
|
||||
msgstr "URL suivante invalide"
|
||||
|
||||
#: authentik/lib/sync/outgoing/models.py
|
||||
msgid "Controls the number of objects synced in a single task"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/lib/sync/outgoing/models.py
|
||||
msgid "Timeout for synchronization of a single page"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/lib/sync/outgoing/models.py
|
||||
msgid ""
|
||||
"When enabled, provider will not modify or create objects in the remote "
|
||||
@@ -1633,6 +1770,10 @@ msgstr "Avatar de l'utilisateu"
|
||||
msgid "Not you?"
|
||||
msgstr "Pas vous ?"
|
||||
|
||||
#: authentik/policies/templates/policies/denied.html
|
||||
msgid "Error"
|
||||
msgstr "Erreur"
|
||||
|
||||
#: authentik/policies/templates/policies/denied.html
|
||||
msgid "Request has been denied."
|
||||
msgstr "La requête a été refusée."
|
||||
@@ -2019,14 +2160,6 @@ msgstr "Jeton de rafraîchissement OAuth2"
|
||||
msgid "OAuth2 Refresh Tokens"
|
||||
msgstr "Jetons de rafraîchissement OAuth2"
|
||||
|
||||
#: authentik/providers/oauth2/models.py
|
||||
msgid "Device Token"
|
||||
msgstr "Jeton d'équipement"
|
||||
|
||||
#: authentik/providers/oauth2/models.py
|
||||
msgid "Device Tokens"
|
||||
msgstr "Jetons d'équipement"
|
||||
|
||||
#: authentik/providers/oauth2/tasks.py
|
||||
msgid "Send a back-channel logout request to the registered client"
|
||||
msgstr "Envoyer une requête de déconnexion Back-Channel au client enregistré"
|
||||
@@ -2266,18 +2399,6 @@ msgstr "Échec d'import des métadonnées : {messages}"
|
||||
msgid "ACS URL"
|
||||
msgstr "ACS URL"
|
||||
|
||||
#: authentik/providers/saml/models.py
|
||||
msgid ""
|
||||
"Value of the audience restriction field of the assertion. When left empty, "
|
||||
"no audience restriction will be added."
|
||||
msgstr ""
|
||||
"Valeur du champ de restriction d'audience de l'assertion. Si vide, aucune "
|
||||
"restriction d'audience ne sera ajoutée."
|
||||
|
||||
#: authentik/providers/saml/models.py
|
||||
msgid "Also known as EntityID"
|
||||
msgstr "Aussi appelé EntityID"
|
||||
|
||||
#: authentik/providers/saml/models.py
|
||||
msgid "Service Provider Binding"
|
||||
msgstr "Liaison du fournisseur de services"
|
||||
@@ -2290,6 +2411,18 @@ msgstr ""
|
||||
"Cela détermine la manière dont authentik renvoie la réponse au fournisseur "
|
||||
"de services."
|
||||
|
||||
#: authentik/providers/saml/models.py
|
||||
msgid ""
|
||||
"Value of the audience restriction field of the assertion. When left empty, "
|
||||
"no audience restriction will be added."
|
||||
msgstr ""
|
||||
"Valeur du champ de restriction d'audience de l'assertion. Si vide, aucune "
|
||||
"restriction d'audience ne sera ajoutée."
|
||||
|
||||
#: authentik/providers/saml/models.py
|
||||
msgid "Also known as EntityID"
|
||||
msgstr "Aussi appelé EntityID"
|
||||
|
||||
#: authentik/providers/saml/models.py
|
||||
msgid "SLS URL"
|
||||
msgstr "URL SLS"
|
||||
Binary file not shown.
BIN
locale/it_IT/LC_MESSAGES/django.mo
Normal file
BIN
locale/it_IT/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
@@ -11,23 +11,23 @@
|
||||
# Nicola Mersi, 2024
|
||||
# tmassimi, 2024
|
||||
# Marc Schmitt, 2024
|
||||
# Kowalski Dragon (kowalski7cc) <kowalski.7cc@gmail.com>, 2025
|
||||
# Matteo Piccina <altermatte@gmail.com>, 2025
|
||||
# albanobattistella <albanobattistella@gmail.com>, 2025
|
||||
# Kowalski Dragon (kowalski7cc) <kowalski.7cc@gmail.com>, 2025
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-10-20 00:11+0000\n"
|
||||
"POT-Creation-Date: 2025-12-01 15:07+0000\n"
|
||||
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
|
||||
"Last-Translator: albanobattistella <albanobattistella@gmail.com>, 2025\n"
|
||||
"Last-Translator: Kowalski Dragon (kowalski7cc) <kowalski.7cc@gmail.com>, 2025\n"
|
||||
"Language-Team: Italian (https://app.transifex.com/authentik/teams/119923/it/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Language: it\n"
|
||||
"Language: it_IT\n"
|
||||
"Plural-Forms: nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\n"
|
||||
|
||||
#: authentik/admin/models.py
|
||||
@@ -389,6 +389,12 @@ msgstr "Nome visualizzato della sorgente."
|
||||
msgid "Internal source name, used in URLs."
|
||||
msgstr "Nome interno della sorgente, utilizzato negli URL."
|
||||
|
||||
#: authentik/core/models.py
|
||||
msgid ""
|
||||
"When enabled, this source will be displayed as a prominent button on the "
|
||||
"login page, instead of a small icon."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/models.py
|
||||
msgid "Flow to use when authenticating existing users."
|
||||
msgstr "Flusso da usare per autenticare utenti esistenti."
|
||||
@@ -421,7 +427,7 @@ msgstr "Token"
|
||||
msgid "Tokens"
|
||||
msgstr "Tokens"
|
||||
|
||||
#: authentik/core/models.py
|
||||
#: authentik/core/models.py authentik/endpoints/connectors/agent/models.py
|
||||
msgid "View token's key"
|
||||
msgstr "Visualizza la chiave token"
|
||||
|
||||
@@ -493,6 +499,7 @@ msgid "Remove temporary users created by SAML Sources."
|
||||
msgstr "Rimuovi gli utenti temporanei creati da SAML Sources."
|
||||
|
||||
#: authentik/core/templates/if/error.html
|
||||
#: authentik/policies/templates/policies/denied.html
|
||||
msgid "Go home"
|
||||
msgstr "Vai alla pagina iniziale"
|
||||
|
||||
@@ -524,6 +531,26 @@ msgstr "rsa"
|
||||
msgid "ecdsa"
|
||||
msgstr "ecdsa"
|
||||
|
||||
#: authentik/crypto/models.py
|
||||
msgid "RSA"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/crypto/models.py
|
||||
msgid "Elliptic Curve"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/crypto/models.py
|
||||
msgid "DSA"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/crypto/models.py
|
||||
msgid "Ed25519"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/crypto/models.py
|
||||
msgid "Ed448"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/crypto/models.py
|
||||
msgid "PEM-encoded Certificate data"
|
||||
msgstr "Dati del certificato in codifica PEM"
|
||||
@@ -548,6 +575,104 @@ msgstr "Coppie certificato-chiave"
|
||||
msgid "Discover, import and update certificates from the filesystem."
|
||||
msgstr "Scopri, importa e aggiorna i certificati dal file system."
|
||||
|
||||
#: authentik/endpoints/connectors/agent/api/connectors.py
|
||||
msgid "Selected platform not supported"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/api/connectors.py
|
||||
msgid "Token is expired"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/api/connectors.py
|
||||
msgid "Invalid token for connector"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
msgid "Agent Connector"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
msgid "Agent Connectors"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
#: authentik/providers/oauth2/models.py
|
||||
msgid "Device Token"
|
||||
msgstr "Token Dispositivo"
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
#: authentik/providers/oauth2/models.py
|
||||
msgid "Device Tokens"
|
||||
msgstr "Token Dispositivi"
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
msgid "Enrollment Token"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
msgid "Enrollment Tokens"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
msgid "Device authentication token"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
msgid "Device authentication tokens"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
msgid "Apple Nonce"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
msgid "Apple Nonces"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device"
|
||||
msgstr "Dispositivo"
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Devices"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device User binding"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device User bindings"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device connection"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device connections"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device fact snapshot"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device fact snapshots"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device access group"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device access groups"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/tasks.py
|
||||
msgid "Sync endpoints."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/enterprise/api.py
|
||||
msgid "Enterprise is required to create/update this object."
|
||||
msgstr "Versione Enterprise richiesta per creare/aggiornare questo oggetto"
|
||||
@@ -1211,10 +1336,22 @@ msgstr "Token del flusso"
|
||||
msgid "Flow Tokens"
|
||||
msgstr "Tokens del flusso"
|
||||
|
||||
#: authentik/flows/templates/if/flow.html
|
||||
msgid "Site footer"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/flows/views/executor.py
|
||||
msgid "Invalid next URL"
|
||||
msgstr "URL successivo non valido"
|
||||
|
||||
#: authentik/lib/sync/outgoing/models.py
|
||||
msgid "Controls the number of objects synced in a single task"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/lib/sync/outgoing/models.py
|
||||
msgid "Timeout for synchronization of a single page"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/lib/sync/outgoing/models.py
|
||||
msgid ""
|
||||
"When enabled, provider will not modify or create objects in the remote "
|
||||
@@ -1613,6 +1750,10 @@ msgstr "Avatar utente"
|
||||
msgid "Not you?"
|
||||
msgstr "Non sei tu?"
|
||||
|
||||
#: authentik/policies/templates/policies/denied.html
|
||||
msgid "Error"
|
||||
msgstr "Errore"
|
||||
|
||||
#: authentik/policies/templates/policies/denied.html
|
||||
msgid "Request has been denied."
|
||||
msgstr "La richiesta è stata negata."
|
||||
@@ -1994,14 +2135,6 @@ msgstr "Token di aggiornamento OAuth2"
|
||||
msgid "OAuth2 Refresh Tokens"
|
||||
msgstr "Tokens di aggiornamento OAuth2"
|
||||
|
||||
#: authentik/providers/oauth2/models.py
|
||||
msgid "Device Token"
|
||||
msgstr "Token Dispositivo"
|
||||
|
||||
#: authentik/providers/oauth2/models.py
|
||||
msgid "Device Tokens"
|
||||
msgstr "Token Dispositivi"
|
||||
|
||||
#: authentik/providers/oauth2/tasks.py
|
||||
msgid "Send a back-channel logout request to the registered client"
|
||||
msgstr ""
|
||||
@@ -2238,18 +2371,6 @@ msgstr "Impossibile importare i metadati: {messages}"
|
||||
msgid "ACS URL"
|
||||
msgstr "URL ACS"
|
||||
|
||||
#: authentik/providers/saml/models.py
|
||||
msgid ""
|
||||
"Value of the audience restriction field of the assertion. When left empty, "
|
||||
"no audience restriction will be added."
|
||||
msgstr ""
|
||||
"Valore del campo di limitazione del pubblico dell'asserzione. Se lasciato "
|
||||
"vuoto, non verrà aggiunta alcuna restrizione sul pubblico."
|
||||
|
||||
#: authentik/providers/saml/models.py
|
||||
msgid "Also known as EntityID"
|
||||
msgstr "Conosciuto anche come EntityID"
|
||||
|
||||
#: authentik/providers/saml/models.py
|
||||
msgid "Service Provider Binding"
|
||||
msgstr "Associazione fornitore di servizi"
|
||||
@@ -2262,6 +2383,18 @@ msgstr ""
|
||||
"Ciò determina il modo in cui authentik invia la risposta al fornitore di "
|
||||
"servizi."
|
||||
|
||||
#: authentik/providers/saml/models.py
|
||||
msgid ""
|
||||
"Value of the audience restriction field of the assertion. When left empty, "
|
||||
"no audience restriction will be added."
|
||||
msgstr ""
|
||||
"Valore del campo di limitazione del pubblico dell'asserzione. Se lasciato "
|
||||
"vuoto, non verrà aggiunta alcuna restrizione sul pubblico."
|
||||
|
||||
#: authentik/providers/saml/models.py
|
||||
msgid "Also known as EntityID"
|
||||
msgstr "Conosciuto anche come EntityID"
|
||||
|
||||
#: authentik/providers/saml/models.py
|
||||
msgid "SLS URL"
|
||||
msgstr ""
|
||||
@@ -19,14 +19,14 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-11-10 00:11+0000\n"
|
||||
"POT-Creation-Date: 2025-12-01 15:07+0000\n"
|
||||
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
|
||||
"Last-Translator: Teffen Ellis, 2025\n"
|
||||
"Language-Team: Japanese (https://app.transifex.com/authentik/teams/119923/ja/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Language: ja\n"
|
||||
"Language: ja_JP\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
|
||||
#: authentik/admin/models.py
|
||||
@@ -363,6 +363,12 @@ msgstr "ソースの表示名。"
|
||||
msgid "Internal source name, used in URLs."
|
||||
msgstr "URLで使用される内部ソース名。"
|
||||
|
||||
#: authentik/core/models.py
|
||||
msgid ""
|
||||
"When enabled, this source will be displayed as a prominent button on the "
|
||||
"login page, instead of a small icon."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/models.py
|
||||
msgid "Flow to use when authenticating existing users."
|
||||
msgstr "フローは既存のユーザーを認証するときに使用されます。"
|
||||
@@ -391,7 +397,7 @@ msgstr "トークン"
|
||||
msgid "Tokens"
|
||||
msgstr "トークン"
|
||||
|
||||
#: authentik/core/models.py
|
||||
#: authentik/core/models.py authentik/endpoints/connectors/agent/models.py
|
||||
msgid "View token's key"
|
||||
msgstr "トークンのキーを表示"
|
||||
|
||||
@@ -461,6 +467,7 @@ msgid "Remove temporary users created by SAML Sources."
|
||||
msgstr "SAMLで作成された一時ユーザを削除。"
|
||||
|
||||
#: authentik/core/templates/if/error.html
|
||||
#: authentik/policies/templates/policies/denied.html
|
||||
msgid "Go home"
|
||||
msgstr "ホームに戻る"
|
||||
|
||||
@@ -534,6 +541,104 @@ msgstr "証明書とキーのペア"
|
||||
msgid "Discover, import and update certificates from the filesystem."
|
||||
msgstr "証明書をファイルシステムから検出、インポート、更新する。"
|
||||
|
||||
#: authentik/endpoints/connectors/agent/api/connectors.py
|
||||
msgid "Selected platform not supported"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/api/connectors.py
|
||||
msgid "Token is expired"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/api/connectors.py
|
||||
msgid "Invalid token for connector"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
msgid "Agent Connector"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
msgid "Agent Connectors"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
#: authentik/providers/oauth2/models.py
|
||||
msgid "Device Token"
|
||||
msgstr "デバイストークン"
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
#: authentik/providers/oauth2/models.py
|
||||
msgid "Device Tokens"
|
||||
msgstr "デバイストークン"
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
msgid "Enrollment Token"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
msgid "Enrollment Tokens"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
msgid "Device authentication token"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
msgid "Device authentication tokens"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
msgid "Apple Nonce"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
msgid "Apple Nonces"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Devices"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device User binding"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device User bindings"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device connection"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device connections"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device fact snapshot"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device fact snapshots"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device access group"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device access groups"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/tasks.py
|
||||
msgid "Sync endpoints."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/enterprise/api.py
|
||||
msgid "Enterprise is required to create/update this object."
|
||||
msgstr "このオブジェクトの作成/更新にはエンタープライズ契約が必要です。"
|
||||
@@ -1143,6 +1248,10 @@ msgstr "フロートークン"
|
||||
msgid "Flow Tokens"
|
||||
msgstr "フロートークン"
|
||||
|
||||
#: authentik/flows/templates/if/flow.html
|
||||
msgid "Site footer"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/flows/views/executor.py
|
||||
msgid "Invalid next URL"
|
||||
msgstr "無効なネクスト URL"
|
||||
@@ -1522,6 +1631,10 @@ msgstr "アバター"
|
||||
msgid "Not you?"
|
||||
msgstr "あなたではありませんか?"
|
||||
|
||||
#: authentik/policies/templates/policies/denied.html
|
||||
msgid "Error"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/policies/templates/policies/denied.html
|
||||
msgid "Request has been denied."
|
||||
msgstr "リクエストは拒否されました。"
|
||||
@@ -1877,14 +1990,6 @@ msgstr "OAuth2 リフレッシュトークン"
|
||||
msgid "OAuth2 Refresh Tokens"
|
||||
msgstr "OAuth2 リフレッシュトークン"
|
||||
|
||||
#: authentik/providers/oauth2/models.py
|
||||
msgid "Device Token"
|
||||
msgstr "デバイストークン"
|
||||
|
||||
#: authentik/providers/oauth2/models.py
|
||||
msgid "Device Tokens"
|
||||
msgstr "デバイストークン"
|
||||
|
||||
#: authentik/providers/oauth2/tasks.py
|
||||
msgid "Send a back-channel logout request to the registered client"
|
||||
msgstr "登録されたクライアントにバックチャネルログアウトリクエストを送信"
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
BIN
locale/nl_NL/LC_MESSAGES/django.mo
Normal file
BIN
locale/nl_NL/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
@@ -11,22 +11,22 @@
|
||||
# Sjors Wortelboer, 2024
|
||||
# Marc Schmitt, 2025
|
||||
# Taeke <transifex.net@taeke.eu>, 2025
|
||||
# Alex Kruidenberg <alexkruidenberg@hotmail.com>, 2025
|
||||
# Dany Sluijk, 2025
|
||||
# Alex Kruidenberg <alexkruidenberg@hotmail.com>, 2025
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-10-20 00:11+0000\n"
|
||||
"POT-Creation-Date: 2025-12-01 15:07+0000\n"
|
||||
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
|
||||
"Last-Translator: Dany Sluijk, 2025\n"
|
||||
"Last-Translator: Alex Kruidenberg <alexkruidenberg@hotmail.com>, 2025\n"
|
||||
"Language-Team: Dutch (https://app.transifex.com/authentik/teams/119923/nl/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Language: nl\n"
|
||||
"Language: nl_NL\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: authentik/admin/models.py
|
||||
@@ -388,6 +388,12 @@ msgstr "Weergavenaam van de bron."
|
||||
msgid "Internal source name, used in URLs."
|
||||
msgstr "Interne naam van de bron, gebruikt in URL's."
|
||||
|
||||
#: authentik/core/models.py
|
||||
msgid ""
|
||||
"When enabled, this source will be displayed as a prominent button on the "
|
||||
"login page, instead of a small icon."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/models.py
|
||||
msgid "Flow to use when authenticating existing users."
|
||||
msgstr "Flow om te gebruiken bij het authenticeren van bestaande gebruikers."
|
||||
@@ -420,7 +426,7 @@ msgstr "Token"
|
||||
msgid "Tokens"
|
||||
msgstr "Tokens"
|
||||
|
||||
#: authentik/core/models.py
|
||||
#: authentik/core/models.py authentik/endpoints/connectors/agent/models.py
|
||||
msgid "View token's key"
|
||||
msgstr "Toon token sleutel"
|
||||
|
||||
@@ -492,6 +498,7 @@ msgid "Remove temporary users created by SAML Sources."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/templates/if/error.html
|
||||
#: authentik/policies/templates/policies/denied.html
|
||||
msgid "Go home"
|
||||
msgstr "Ga naar startpagina"
|
||||
|
||||
@@ -523,6 +530,26 @@ msgstr "rsa"
|
||||
msgid "ecdsa"
|
||||
msgstr "ecdsa"
|
||||
|
||||
#: authentik/crypto/models.py
|
||||
msgid "RSA"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/crypto/models.py
|
||||
msgid "Elliptic Curve"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/crypto/models.py
|
||||
msgid "DSA"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/crypto/models.py
|
||||
msgid "Ed25519"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/crypto/models.py
|
||||
msgid "Ed448"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/crypto/models.py
|
||||
msgid "PEM-encoded Certificate data"
|
||||
msgstr "PEM-gecodeerde certificaatgegevens"
|
||||
@@ -547,6 +574,104 @@ msgstr "Certificaat-Sleutelparen"
|
||||
msgid "Discover, import and update certificates from the filesystem."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/api/connectors.py
|
||||
msgid "Selected platform not supported"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/api/connectors.py
|
||||
msgid "Token is expired"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/api/connectors.py
|
||||
msgid "Invalid token for connector"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
msgid "Agent Connector"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
msgid "Agent Connectors"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
#: authentik/providers/oauth2/models.py
|
||||
msgid "Device Token"
|
||||
msgstr "Apparaattoken"
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
#: authentik/providers/oauth2/models.py
|
||||
msgid "Device Tokens"
|
||||
msgstr "Apparaattokens"
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
msgid "Enrollment Token"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
msgid "Enrollment Tokens"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
msgid "Device authentication token"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
msgid "Device authentication tokens"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
msgid "Apple Nonce"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
msgid "Apple Nonces"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Devices"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device User binding"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device User bindings"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device connection"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device connections"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device fact snapshot"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device fact snapshots"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device access group"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device access groups"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/tasks.py
|
||||
msgid "Sync endpoints."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/enterprise/api.py
|
||||
msgid "Enterprise is required to create/update this object."
|
||||
msgstr "Enterprise is benodigd om dit object te aan te maken/bijwerken."
|
||||
@@ -1177,10 +1302,22 @@ msgstr "Flowtoken"
|
||||
msgid "Flow Tokens"
|
||||
msgstr "Flowtokens"
|
||||
|
||||
#: authentik/flows/templates/if/flow.html
|
||||
msgid "Site footer"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/flows/views/executor.py
|
||||
msgid "Invalid next URL"
|
||||
msgstr "Invalide volgend URL"
|
||||
|
||||
#: authentik/lib/sync/outgoing/models.py
|
||||
msgid "Controls the number of objects synced in a single task"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/lib/sync/outgoing/models.py
|
||||
msgid "Timeout for synchronization of a single page"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/lib/sync/outgoing/models.py
|
||||
msgid ""
|
||||
"When enabled, provider will not modify or create objects in the remote "
|
||||
@@ -1573,6 +1710,10 @@ msgstr "Avatar van de gebruiker"
|
||||
msgid "Not you?"
|
||||
msgstr "Niet jij?"
|
||||
|
||||
#: authentik/policies/templates/policies/denied.html
|
||||
msgid "Error"
|
||||
msgstr "Fout"
|
||||
|
||||
#: authentik/policies/templates/policies/denied.html
|
||||
msgid "Request has been denied."
|
||||
msgstr "Verzoek is geweigerd."
|
||||
@@ -1953,14 +2094,6 @@ msgstr "OAuth2 Verversingstoken"
|
||||
msgid "OAuth2 Refresh Tokens"
|
||||
msgstr "OAuth2 Verversingstokens"
|
||||
|
||||
#: authentik/providers/oauth2/models.py
|
||||
msgid "Device Token"
|
||||
msgstr "Apparaattoken"
|
||||
|
||||
#: authentik/providers/oauth2/models.py
|
||||
msgid "Device Tokens"
|
||||
msgstr "Apparaattokens"
|
||||
|
||||
#: authentik/providers/oauth2/tasks.py
|
||||
msgid "Send a back-channel logout request to the registered client"
|
||||
msgstr ""
|
||||
@@ -2193,6 +2326,17 @@ msgstr ""
|
||||
msgid "ACS URL"
|
||||
msgstr "ACS-URL"
|
||||
|
||||
#: authentik/providers/saml/models.py
|
||||
msgid "Service Provider Binding"
|
||||
msgstr "Serviceproviderbinding"
|
||||
|
||||
#: authentik/providers/saml/models.py
|
||||
msgid ""
|
||||
"This determines how authentik sends the response back to the Service "
|
||||
"Provider."
|
||||
msgstr ""
|
||||
"Dit bepaalt hoe authentik de reactie terugstuurt naar de serviceprovider."
|
||||
|
||||
#: authentik/providers/saml/models.py
|
||||
msgid ""
|
||||
"Value of the audience restriction field of the assertion. When left empty, "
|
||||
@@ -2205,17 +2349,6 @@ msgstr ""
|
||||
msgid "Also known as EntityID"
|
||||
msgstr "Ook bekend als EntityID"
|
||||
|
||||
#: authentik/providers/saml/models.py
|
||||
msgid "Service Provider Binding"
|
||||
msgstr "Serviceproviderbinding"
|
||||
|
||||
#: authentik/providers/saml/models.py
|
||||
msgid ""
|
||||
"This determines how authentik sends the response back to the Service "
|
||||
"Provider."
|
||||
msgstr ""
|
||||
"Dit bepaalt hoe authentik de reactie terugstuurt naar de serviceprovider."
|
||||
|
||||
#: authentik/providers/saml/models.py
|
||||
msgid "SLS URL"
|
||||
msgstr ""
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -11,18 +11,18 @@
|
||||
# Hacklab, 2025
|
||||
# Victor Haddad, 2025
|
||||
# Josenivaldo Benito Junior, 2025
|
||||
# Rafael Mundel, 2025
|
||||
# Hudson Oliveira, 2025
|
||||
# Wagner Santos, 2025
|
||||
# Rafael Mundel, 2025
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-10-20 00:11+0000\n"
|
||||
"POT-Creation-Date: 2025-12-01 15:07+0000\n"
|
||||
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
|
||||
"Last-Translator: Wagner Santos, 2025\n"
|
||||
"Last-Translator: Rafael Mundel, 2025\n"
|
||||
"Language-Team: Portuguese (Brazil) (https://app.transifex.com/authentik/teams/119923/pt_BR/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
@@ -390,6 +390,12 @@ msgstr "Nome de exibição da fonte."
|
||||
msgid "Internal source name, used in URLs."
|
||||
msgstr "Nome da fonte interna, usado em URLs."
|
||||
|
||||
#: authentik/core/models.py
|
||||
msgid ""
|
||||
"When enabled, this source will be displayed as a prominent button on the "
|
||||
"login page, instead of a small icon."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/models.py
|
||||
msgid "Flow to use when authenticating existing users."
|
||||
msgstr "Fluxo a ser usado ao autenticar usuários existentes."
|
||||
@@ -422,7 +428,7 @@ msgstr "Token"
|
||||
msgid "Tokens"
|
||||
msgstr "Tokens"
|
||||
|
||||
#: authentik/core/models.py
|
||||
#: authentik/core/models.py authentik/endpoints/connectors/agent/models.py
|
||||
msgid "View token's key"
|
||||
msgstr "Ver chaves do token"
|
||||
|
||||
@@ -494,6 +500,7 @@ msgid "Remove temporary users created by SAML Sources."
|
||||
msgstr "Remover usuários temporários criados por Fontes SAML."
|
||||
|
||||
#: authentik/core/templates/if/error.html
|
||||
#: authentik/policies/templates/policies/denied.html
|
||||
msgid "Go home"
|
||||
msgstr "Ir para casa"
|
||||
|
||||
@@ -525,6 +532,26 @@ msgstr "rsa"
|
||||
msgid "ecdsa"
|
||||
msgstr "ecdsa"
|
||||
|
||||
#: authentik/crypto/models.py
|
||||
msgid "RSA"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/crypto/models.py
|
||||
msgid "Elliptic Curve"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/crypto/models.py
|
||||
msgid "DSA"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/crypto/models.py
|
||||
msgid "Ed25519"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/crypto/models.py
|
||||
msgid "Ed448"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/crypto/models.py
|
||||
msgid "PEM-encoded Certificate data"
|
||||
msgstr "Dados de certificado codificados por PEM"
|
||||
@@ -549,6 +576,104 @@ msgstr "Pares de chave de certificado"
|
||||
msgid "Discover, import and update certificates from the filesystem."
|
||||
msgstr "Descobrir, importar e atualizar certificados do sistema de arquivos."
|
||||
|
||||
#: authentik/endpoints/connectors/agent/api/connectors.py
|
||||
msgid "Selected platform not supported"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/api/connectors.py
|
||||
msgid "Token is expired"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/api/connectors.py
|
||||
msgid "Invalid token for connector"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
msgid "Agent Connector"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
msgid "Agent Connectors"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
#: authentik/providers/oauth2/models.py
|
||||
msgid "Device Token"
|
||||
msgstr "Token do dispositivo"
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
#: authentik/providers/oauth2/models.py
|
||||
msgid "Device Tokens"
|
||||
msgstr "Tokens de dispositivo"
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
msgid "Enrollment Token"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
msgid "Enrollment Tokens"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
msgid "Device authentication token"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
msgid "Device authentication tokens"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
msgid "Apple Nonce"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
msgid "Apple Nonces"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device"
|
||||
msgstr "Dispositivo"
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Devices"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device User binding"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device User bindings"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device connection"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device connections"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device fact snapshot"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device fact snapshots"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device access group"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device access groups"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/tasks.py
|
||||
msgid "Sync endpoints."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/enterprise/api.py
|
||||
msgid "Enterprise is required to create/update this object."
|
||||
msgstr "Enterprise é necessário para criar/atualizar esse objeto."
|
||||
@@ -1201,10 +1326,22 @@ msgstr "Token de Fluxo"
|
||||
msgid "Flow Tokens"
|
||||
msgstr "Tokens de Fluxo"
|
||||
|
||||
#: authentik/flows/templates/if/flow.html
|
||||
msgid "Site footer"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/flows/views/executor.py
|
||||
msgid "Invalid next URL"
|
||||
msgstr "URL de próximo passo inválida"
|
||||
|
||||
#: authentik/lib/sync/outgoing/models.py
|
||||
msgid "Controls the number of objects synced in a single task"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/lib/sync/outgoing/models.py
|
||||
msgid "Timeout for synchronization of a single page"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/lib/sync/outgoing/models.py
|
||||
msgid ""
|
||||
"When enabled, provider will not modify or create objects in the remote "
|
||||
@@ -1599,6 +1736,10 @@ msgstr "Avatar do usuário"
|
||||
msgid "Not you?"
|
||||
msgstr "Não é você?"
|
||||
|
||||
#: authentik/policies/templates/policies/denied.html
|
||||
msgid "Error"
|
||||
msgstr "Erro"
|
||||
|
||||
#: authentik/policies/templates/policies/denied.html
|
||||
msgid "Request has been denied."
|
||||
msgstr "A solicitação foi negada."
|
||||
@@ -1978,14 +2119,6 @@ msgstr "Token de Atualização OAuth2"
|
||||
msgid "OAuth2 Refresh Tokens"
|
||||
msgstr "Tokens de Atualização OAuth2"
|
||||
|
||||
#: authentik/providers/oauth2/models.py
|
||||
msgid "Device Token"
|
||||
msgstr "Token do dispositivo"
|
||||
|
||||
#: authentik/providers/oauth2/models.py
|
||||
msgid "Device Tokens"
|
||||
msgstr "Tokens de dispositivo"
|
||||
|
||||
#: authentik/providers/oauth2/tasks.py
|
||||
msgid "Send a back-channel logout request to the registered client"
|
||||
msgstr ""
|
||||
@@ -2223,18 +2356,6 @@ msgstr "Falha ao importar Metadata: {messages}"
|
||||
msgid "ACS URL"
|
||||
msgstr "URL ACS"
|
||||
|
||||
#: authentik/providers/saml/models.py
|
||||
msgid ""
|
||||
"Value of the audience restriction field of the assertion. When left empty, "
|
||||
"no audience restriction will be added."
|
||||
msgstr ""
|
||||
"Valor do campo de restrição de público da asserção. Quando deixado em "
|
||||
"branco, nenhuma restrição de público será adicionada."
|
||||
|
||||
#: authentik/providers/saml/models.py
|
||||
msgid "Also known as EntityID"
|
||||
msgstr "Também conhecido como EntityID"
|
||||
|
||||
#: authentik/providers/saml/models.py
|
||||
msgid "Service Provider Binding"
|
||||
msgstr "Vinculação do Provedor de Serviços"
|
||||
@@ -2247,6 +2368,18 @@ msgstr ""
|
||||
"Isso determina como o authentik envia a resposta de volta ao provedor de "
|
||||
"serviços."
|
||||
|
||||
#: authentik/providers/saml/models.py
|
||||
msgid ""
|
||||
"Value of the audience restriction field of the assertion. When left empty, "
|
||||
"no audience restriction will be added."
|
||||
msgstr ""
|
||||
"Valor do campo de restrição de público da asserção. Quando deixado em "
|
||||
"branco, nenhuma restrição de público será adicionada."
|
||||
|
||||
#: authentik/providers/saml/models.py
|
||||
msgid "Also known as EntityID"
|
||||
msgstr "Também conhecido como EntityID"
|
||||
|
||||
#: authentik/providers/saml/models.py
|
||||
msgid "SLS URL"
|
||||
msgstr "URLs SLS"
|
||||
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -18,14 +18,14 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-10-20 00:11+0000\n"
|
||||
"POT-Creation-Date: 2025-12-01 15:07+0000\n"
|
||||
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
|
||||
"Last-Translator: Marc Schmitt, 2025\n"
|
||||
"Language-Team: Russian (https://app.transifex.com/authentik/teams/119923/ru/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Language: ru\n"
|
||||
"Language: ru_RU\n"
|
||||
"Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n%100>=11 && n%100<=14)? 2 : 3);\n"
|
||||
|
||||
#: authentik/admin/models.py
|
||||
@@ -388,6 +388,12 @@ msgstr "Отображаемое имя источника."
|
||||
msgid "Internal source name, used in URLs."
|
||||
msgstr "Внутреннее имя источника, используемое в URL-адресах."
|
||||
|
||||
#: authentik/core/models.py
|
||||
msgid ""
|
||||
"When enabled, this source will be displayed as a prominent button on the "
|
||||
"login page, instead of a small icon."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/models.py
|
||||
msgid "Flow to use when authenticating existing users."
|
||||
msgstr "Поток, используемый при аутентификации существующих пользователей."
|
||||
@@ -420,7 +426,7 @@ msgstr "Токен"
|
||||
msgid "Tokens"
|
||||
msgstr "Токены"
|
||||
|
||||
#: authentik/core/models.py
|
||||
#: authentik/core/models.py authentik/endpoints/connectors/agent/models.py
|
||||
msgid "View token's key"
|
||||
msgstr "Просмотр ключа токена "
|
||||
|
||||
@@ -493,6 +499,7 @@ msgid "Remove temporary users created by SAML Sources."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/templates/if/error.html
|
||||
#: authentik/policies/templates/policies/denied.html
|
||||
msgid "Go home"
|
||||
msgstr "Домой"
|
||||
|
||||
@@ -524,6 +531,26 @@ msgstr "rsa"
|
||||
msgid "ecdsa"
|
||||
msgstr "ecdsa"
|
||||
|
||||
#: authentik/crypto/models.py
|
||||
msgid "RSA"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/crypto/models.py
|
||||
msgid "Elliptic Curve"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/crypto/models.py
|
||||
msgid "DSA"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/crypto/models.py
|
||||
msgid "Ed25519"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/crypto/models.py
|
||||
msgid "Ed448"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/crypto/models.py
|
||||
msgid "PEM-encoded Certificate data"
|
||||
msgstr "Данные сертификата, закодированные в формате PEM"
|
||||
@@ -548,6 +575,104 @@ msgstr "Пары сертификат-ключ"
|
||||
msgid "Discover, import and update certificates from the filesystem."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/api/connectors.py
|
||||
msgid "Selected platform not supported"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/api/connectors.py
|
||||
msgid "Token is expired"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/api/connectors.py
|
||||
msgid "Invalid token for connector"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
msgid "Agent Connector"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
msgid "Agent Connectors"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
#: authentik/providers/oauth2/models.py
|
||||
msgid "Device Token"
|
||||
msgstr "Токен устройства"
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
#: authentik/providers/oauth2/models.py
|
||||
msgid "Device Tokens"
|
||||
msgstr "Токены устройства"
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
msgid "Enrollment Token"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
msgid "Enrollment Tokens"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
msgid "Device authentication token"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
msgid "Device authentication tokens"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
msgid "Apple Nonce"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/connectors/agent/models.py
|
||||
msgid "Apple Nonces"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Devices"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device User binding"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device User bindings"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device connection"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device connections"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device fact snapshot"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device fact snapshots"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device access group"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Device access groups"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/tasks.py
|
||||
msgid "Sync endpoints."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/enterprise/api.py
|
||||
msgid "Enterprise is required to create/update this object."
|
||||
msgstr "Для создания/обновления этого объекта требуется Enterprise."
|
||||
@@ -1176,10 +1301,22 @@ msgstr "Токен потока"
|
||||
msgid "Flow Tokens"
|
||||
msgstr "Токены потока"
|
||||
|
||||
#: authentik/flows/templates/if/flow.html
|
||||
msgid "Site footer"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/flows/views/executor.py
|
||||
msgid "Invalid next URL"
|
||||
msgstr "Недопустимый следующий URL-адрес"
|
||||
|
||||
#: authentik/lib/sync/outgoing/models.py
|
||||
msgid "Controls the number of objects synced in a single task"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/lib/sync/outgoing/models.py
|
||||
msgid "Timeout for synchronization of a single page"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/lib/sync/outgoing/models.py
|
||||
msgid ""
|
||||
"When enabled, provider will not modify or create objects in the remote "
|
||||
@@ -1574,6 +1711,10 @@ msgstr "Аватар пользователя"
|
||||
msgid "Not you?"
|
||||
msgstr "Не вы?"
|
||||
|
||||
#: authentik/policies/templates/policies/denied.html
|
||||
msgid "Error"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/policies/templates/policies/denied.html
|
||||
msgid "Request has been denied."
|
||||
msgstr "В произведении запроса было отказано."
|
||||
@@ -1952,14 +2093,6 @@ msgstr "OAuth2 Refresh токен"
|
||||
msgid "OAuth2 Refresh Tokens"
|
||||
msgstr "OAuth2 Refresh токены"
|
||||
|
||||
#: authentik/providers/oauth2/models.py
|
||||
msgid "Device Token"
|
||||
msgstr "Токен устройства"
|
||||
|
||||
#: authentik/providers/oauth2/models.py
|
||||
msgid "Device Tokens"
|
||||
msgstr "Токены устройства"
|
||||
|
||||
#: authentik/providers/oauth2/tasks.py
|
||||
msgid "Send a back-channel logout request to the registered client"
|
||||
msgstr ""
|
||||
@@ -2193,6 +2326,17 @@ msgstr "Не удалось импортировать метаданные: {me
|
||||
msgid "ACS URL"
|
||||
msgstr "URL-адрес ACS"
|
||||
|
||||
#: authentik/providers/saml/models.py
|
||||
msgid "Service Provider Binding"
|
||||
msgstr "Привязка провайдера услуг"
|
||||
|
||||
#: authentik/providers/saml/models.py
|
||||
msgid ""
|
||||
"This determines how authentik sends the response back to the Service "
|
||||
"Provider."
|
||||
msgstr ""
|
||||
"Это определяет, как authentik отправляет ответ обратно провайдеру услуг."
|
||||
|
||||
#: authentik/providers/saml/models.py
|
||||
msgid ""
|
||||
"Value of the audience restriction field of the assertion. When left empty, "
|
||||
@@ -2205,17 +2349,6 @@ msgstr ""
|
||||
msgid "Also known as EntityID"
|
||||
msgstr "Также известен как EntityID"
|
||||
|
||||
#: authentik/providers/saml/models.py
|
||||
msgid "Service Provider Binding"
|
||||
msgstr "Привязка провайдера услуг"
|
||||
|
||||
#: authentik/providers/saml/models.py
|
||||
msgid ""
|
||||
"This determines how authentik sends the response back to the Service "
|
||||
"Provider."
|
||||
msgstr ""
|
||||
"Это определяет, как authentik отправляет ответ обратно провайдеру услуг."
|
||||
|
||||
#: authentik/providers/saml/models.py
|
||||
msgid "SLS URL"
|
||||
msgstr ""
|
||||
Binary file not shown.
BIN
locale/tr_TR/LC_MESSAGES/django.mo
Normal file
BIN
locale/tr_TR/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user