Compare commits
	
		
			134 Commits
		
	
	
		
			dependabot
			...
			version-20
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| a8134e26c8 | |||
| 1c637bc0d1 | |||
| 8af668a9d2 | |||
| 96266e2e2b | |||
| 65373ab217 | |||
| 88590e1134 | |||
| f6ff31e3de | |||
| cbc14524b3 | |||
| d807038d05 | |||
| ba131a50ba | |||
| 0e37a66751 | |||
| 57c220dfc2 | |||
| 7339a22080 | |||
| 8c2d72affe | |||
| 9c31e08bd9 | |||
| 805b9afa80 | |||
| bc809bae1e | |||
| 66773b69ab | |||
| c149701501 | |||
| b13b51b73a | |||
| ee30cc1ede | |||
| de095f4a10 | |||
| abf3aa3a7f | |||
| 3ebae72a76 | |||
| b6b47b669e | |||
| 8422568c42 | |||
| 9973064f50 | |||
| 4fea65f5cc | |||
| 784446a47d | |||
| 516bc65fc4 | |||
| efb59adeff | |||
| 43a2ad66f0 | |||
| ec0c59f1fc | |||
| 8f80072321 | |||
| 33f95c837b | |||
| 43637b8a75 | |||
| 7a4518be26 | |||
| b94fb53821 | |||
| 2be5c9633b | |||
| e729e42595 | |||
| 01d591b84e | |||
| dd08e1bf66 | |||
| 150705f221 | |||
| 6b39f6495e | |||
| 639c57245b | |||
| 730600aea4 | |||
| e15ce5a3f0 | |||
| 1fc91b004b | |||
| 644705e6fe | |||
| ff8ef523db | |||
| 1051dd19ea | |||
| 04cb4fd267 | |||
| da9508f839 | |||
| 841a286a25 | |||
| 63c48d7b99 | |||
| 5994fd2c61 | |||
| 5f745e682e | |||
| 6f1b16e7f9 | |||
| 57bce19e7a | |||
| 850c5d5a45 | |||
| 8b7d11f94c | |||
| 45737909f6 | |||
| 4c5fe84f92 | |||
| 5faa224c81 | |||
| 736da3abef | |||
| 52d90f8d3b | |||
| 7b812de977 | |||
| a4bd2cc263 | |||
| 14038ba8d2 | |||
| eaff59b6b0 | |||
| cb702ca07a | |||
| cb0bfb0dad | |||
| bf46d5c916 | |||
| 59e686c8b9 | |||
| 9e736f2838 | |||
| c2dd3d9c1b | |||
| 42302d3187 | |||
| 20ccabf3ec | |||
| 8f939fa577 | |||
| 2519bcef89 | |||
| 3e3615a859 | |||
| 79e82c8dc9 | |||
| ccd4432e1f | |||
| b3137f5307 | |||
| 2591ed9840 | |||
| b3e89ef570 | |||
| 45b48c5cd6 | |||
| 1eefd834fc | |||
| 4cc6ed97c5 | |||
| bb55d9b3de | |||
| 3972afb865 | |||
| 04a013cc1b | |||
| fb396f7737 | |||
| cf120ff3ff | |||
| 3e4923d52e | |||
| 01793088f0 | |||
| e2bf2ec2cc | |||
| 4dfbe28709 | |||
| b2021a7191 | |||
| 81e5fb0c18 | |||
| a2a2d940a8 | |||
| c034930219 | |||
| da3dc51d87 | |||
| d217a39513 | |||
| 7729a9317c | |||
| be5f5dd3f0 | |||
| bed8d5da4b | |||
| 4f70f84e80 | |||
| 97b8551866 | |||
| 9a0b67e700 | |||
| 97e4c89cec | |||
| 65aedde8f7 | |||
| 17450f23bf | |||
| ab3ad6b7fd | |||
| 45bc3cbd41 | |||
| 9c1bcac6af | |||
| 0a133265c5 | |||
| 57f25a97c9 | |||
| 8f32242787 | |||
| c4bb19051d | |||
| 10f4fae711 | |||
| 2d9eab3f60 | |||
| fa66195619 | |||
| 134eb126b6 | |||
| f5a6136a58 | |||
| 1a82dfcd61 | |||
| 61fc1dc1fb | |||
| 1f921cc18e | |||
| 2f94ee3f1f | |||
| 154fba12e0 | |||
| 0d18c1d797 | |||
| e905dd52d8 | |||
| 245126a1c3 | |||
| 15d84d30ba | 
| @ -1,5 +1,5 @@ | ||||
| [bumpversion] | ||||
| current_version = 2025.4.1 | ||||
| current_version = 2025.6.3 | ||||
| tag = True | ||||
| commit = True | ||||
| parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-(?P<rc_t>[a-zA-Z-]+)(?P<rc_n>[1-9]\\d*))? | ||||
| @ -21,6 +21,8 @@ optional_value = final | ||||
|  | ||||
| [bumpversion:file:package.json] | ||||
|  | ||||
| [bumpversion:file:package-lock.json] | ||||
|  | ||||
| [bumpversion:file:docker-compose.yml] | ||||
|  | ||||
| [bumpversion:file:schema.yml] | ||||
| @ -31,6 +33,4 @@ optional_value = final | ||||
|  | ||||
| [bumpversion:file:internal/constants/constants.go] | ||||
|  | ||||
| [bumpversion:file:web/src/common/constants.ts] | ||||
|  | ||||
| [bumpversion:file:lifecycle/aws/template.yaml] | ||||
|  | ||||
							
								
								
									
										2
									
								
								.github/actions/setup/action.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/actions/setup/action.yml
									
									
									
									
										vendored
									
									
								
							| @ -36,7 +36,7 @@ runs: | ||||
|       with: | ||||
|         go-version-file: "go.mod" | ||||
|     - name: Setup docker cache | ||||
|       uses: ScribeMD/docker-cache@0.5.0 | ||||
|       uses: AndreKurait/docker-cache@0fe76702a40db986d9663c24954fc14c6a6031b7 | ||||
|       with: | ||||
|         key: docker-images-${{ runner.os }}-${{ hashFiles('.github/actions/setup/docker-compose.yml', 'Makefile') }}-${{ inputs.postgresql_version }} | ||||
|     - name: Setup dependencies | ||||
|  | ||||
							
								
								
									
										2
									
								
								.github/workflows/ci-main.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/ci-main.yml
									
									
									
									
										vendored
									
									
								
							| @ -202,7 +202,7 @@ jobs: | ||||
|         uses: actions/cache@v4 | ||||
|         with: | ||||
|           path: web/dist | ||||
|           key: ${{ runner.os }}-web-${{ hashFiles('web/package-lock.json', 'web/src/**', 'web/packages/sfe/src/**') }}-b | ||||
|           key: ${{ runner.os }}-web-${{ hashFiles('web/package-lock.json', 'package-lock.json', 'web/src/**', 'web/packages/sfe/src/**') }}-b | ||||
|       - name: prepare web ui | ||||
|         if: steps.cache-web.outputs.cache-hit != 'true' | ||||
|         working-directory: web | ||||
|  | ||||
							
								
								
									
										1
									
								
								.github/workflows/ci-website.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/workflows/ci-website.yml
									
									
									
									
										vendored
									
									
								
							| @ -49,6 +49,7 @@ jobs: | ||||
|       matrix: | ||||
|         job: | ||||
|           - build | ||||
|           - build:integrations | ||||
|     steps: | ||||
|       - uses: actions/checkout@v4 | ||||
|       - uses: actions/setup-node@v4 | ||||
|  | ||||
							
								
								
									
										2
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
								
							| @ -2,7 +2,7 @@ name: "CodeQL" | ||||
|  | ||||
| on: | ||||
|   push: | ||||
|     branches: [main, "*", next, version*] | ||||
|     branches: [main, next, version*] | ||||
|   pull_request: | ||||
|     branches: [main] | ||||
|   schedule: | ||||
|  | ||||
| @ -96,7 +96,7 @@ RUN --mount=type=secret,id=GEOIPUPDATE_ACCOUNT_ID \ | ||||
| # Stage 5: Download uv | ||||
| FROM ghcr.io/astral-sh/uv:0.7.8 AS uv | ||||
| # Stage 6: Base python image | ||||
| FROM ghcr.io/goauthentik/fips-python:3.13.3-slim-bookworm-fips AS python-base | ||||
| FROM ghcr.io/goauthentik/fips-python:3.13.5-slim-bookworm-fips AS python-base | ||||
|  | ||||
| ENV VENV_PATH="/ak-root/.venv" \ | ||||
|     PATH="/lifecycle:/ak-root/.venv/bin:$PATH" \ | ||||
|  | ||||
							
								
								
									
										4
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								Makefile
									
									
									
									
									
								
							| @ -86,6 +86,10 @@ dev-create-db: | ||||
|  | ||||
| dev-reset: dev-drop-db dev-create-db migrate  ## Drop and restore the Authentik PostgreSQL instance to a "fresh install" state. | ||||
|  | ||||
| update-test-mmdb:  ## Update test GeoIP and ASN Databases | ||||
| 	curl -L https://raw.githubusercontent.com/maxmind/MaxMind-DB/refs/heads/main/test-data/GeoLite2-ASN-Test.mmdb -o ${PWD}/tests/GeoLite2-ASN-Test.mmdb | ||||
| 	curl -L https://raw.githubusercontent.com/maxmind/MaxMind-DB/refs/heads/main/test-data/GeoLite2-City-Test.mmdb -o ${PWD}/tests/GeoLite2-City-Test.mmdb | ||||
|  | ||||
| ######################### | ||||
| ## API Schema | ||||
| ######################### | ||||
|  | ||||
| @ -20,8 +20,8 @@ Even if the issue is not a CVE, we still greatly appreciate your help in hardeni | ||||
|  | ||||
| | Version   | Supported | | ||||
| | --------- | --------- | | ||||
| | 2025.2.x  | ✅        | | ||||
| | 2025.4.x  | ✅        | | ||||
| | 2025.6.x  | ✅        | | ||||
|  | ||||
| ## Reporting a Vulnerability | ||||
|  | ||||
|  | ||||
| @ -2,7 +2,7 @@ | ||||
|  | ||||
| from os import environ | ||||
|  | ||||
| __version__ = "2025.4.1" | ||||
| __version__ = "2025.6.3" | ||||
| ENV_GIT_HASH_KEY = "GIT_BUILD_HASH" | ||||
|  | ||||
|  | ||||
|  | ||||
| @ -148,3 +148,14 @@ class TestBrands(APITestCase): | ||||
|                 "default_locale": "", | ||||
|             }, | ||||
|         ) | ||||
|  | ||||
|     def test_custom_css(self): | ||||
|         """Test custom_css""" | ||||
|         brand = create_test_brand() | ||||
|         brand.branding_custom_css = """* { | ||||
|             font-family: "Foo bar"; | ||||
|         }""" | ||||
|         brand.save() | ||||
|         res = self.client.get(reverse("authentik_core:if-user")) | ||||
|         self.assertEqual(res.status_code, 200) | ||||
|         self.assertIn(brand.branding_custom_css, res.content.decode()) | ||||
|  | ||||
| @ -5,6 +5,8 @@ from typing import Any | ||||
| from django.db.models import F, Q | ||||
| from django.db.models import Value as V | ||||
| from django.http.request import HttpRequest | ||||
| from django.utils.html import _json_script_escapes | ||||
| from django.utils.safestring import mark_safe | ||||
|  | ||||
| from authentik import get_full_version | ||||
| from authentik.brands.models import Brand | ||||
| @ -32,8 +34,13 @@ def context_processor(request: HttpRequest) -> dict[str, Any]: | ||||
|     """Context Processor that injects brand object into every template""" | ||||
|     brand = getattr(request, "brand", DEFAULT_BRAND) | ||||
|     tenant = getattr(request, "tenant", Tenant()) | ||||
|     # similarly to `json_script` we escape everything HTML-related, however django | ||||
|     # only directly exposes this as a function that also wraps it in a <script> tag | ||||
|     # which we dont want for CSS | ||||
|     brand_css = mark_safe(str(brand.branding_custom_css).translate(_json_script_escapes))  # nosec | ||||
|     return { | ||||
|         "brand": brand, | ||||
|         "brand_css": brand_css, | ||||
|         "footer_links": tenant.footer_links, | ||||
|         "html_meta": {**get_http_meta()}, | ||||
|         "version": get_full_version(), | ||||
|  | ||||
| @ -84,6 +84,7 @@ from authentik.flows.views.executor import QS_KEY_TOKEN | ||||
| from authentik.lib.avatars import get_avatar | ||||
| from authentik.rbac.decorators import permission_required | ||||
| from authentik.rbac.models import get_permission_choices | ||||
| from authentik.stages.email.flow import pickle_flow_token_for_email | ||||
| from authentik.stages.email.models import EmailStage | ||||
| from authentik.stages.email.tasks import send_mails | ||||
| from authentik.stages.email.utils import TemplateEmailMessage | ||||
| @ -451,7 +452,7 @@ class UserViewSet(UsedByMixin, ModelViewSet): | ||||
|     def list(self, request, *args, **kwargs): | ||||
|         return super().list(request, *args, **kwargs) | ||||
|  | ||||
|     def _create_recovery_link(self) -> tuple[str, Token]: | ||||
|     def _create_recovery_link(self, for_email=False) -> tuple[str, Token]: | ||||
|         """Create a recovery link (when the current brand has a recovery flow set), | ||||
|         that can either be shown to an admin or sent to the user directly""" | ||||
|         brand: Brand = self.request._request.brand | ||||
| @ -473,12 +474,16 @@ class UserViewSet(UsedByMixin, ModelViewSet): | ||||
|             raise ValidationError( | ||||
|                 {"non_field_errors": "Recovery flow not applicable to user"} | ||||
|             ) from None | ||||
|         _plan = FlowToken.pickle(plan) | ||||
|         if for_email: | ||||
|             _plan = pickle_flow_token_for_email(plan) | ||||
|         token, __ = FlowToken.objects.update_or_create( | ||||
|             identifier=f"{user.uid}-password-reset", | ||||
|             defaults={ | ||||
|                 "user": user, | ||||
|                 "flow": flow, | ||||
|                 "_plan": FlowToken.pickle(plan), | ||||
|                 "_plan": _plan, | ||||
|                 "revoke_on_execution": not for_email, | ||||
|             }, | ||||
|         ) | ||||
|         querystring = urlencode({QS_KEY_TOKEN: token.key}) | ||||
| @ -648,7 +653,7 @@ class UserViewSet(UsedByMixin, ModelViewSet): | ||||
|         if for_user.email == "": | ||||
|             LOGGER.debug("User doesn't have an email address") | ||||
|             raise ValidationError({"non_field_errors": "User does not have an email address set."}) | ||||
|         link, token = self._create_recovery_link() | ||||
|         link, token = self._create_recovery_link(for_email=True) | ||||
|         # Lookup the email stage to assure the current user can access it | ||||
|         stages = get_objects_for_user( | ||||
|             request.user, "authentik_stages_email.view_emailstage" | ||||
|  | ||||
| @ -79,6 +79,7 @@ def _migrate_session( | ||||
|         AuthenticatedSession.objects.using(db_alias).create( | ||||
|             session=session, | ||||
|             user=old_auth_session.user, | ||||
|             uuid=old_auth_session.uuid, | ||||
|         ) | ||||
|  | ||||
|  | ||||
|  | ||||
| @ -1,10 +1,81 @@ | ||||
| # Generated by Django 5.1.9 on 2025-05-14 11:15 | ||||
|  | ||||
| from django.apps.registry import Apps | ||||
| from django.apps.registry import Apps, apps as global_apps | ||||
| from django.db import migrations | ||||
| from django.contrib.contenttypes.management import create_contenttypes | ||||
| from django.contrib.auth.management import create_permissions | ||||
| from django.db.backends.base.schema import BaseDatabaseSchemaEditor | ||||
|  | ||||
|  | ||||
| def migrate_authenticated_session_permissions(apps: Apps, schema_editor: BaseDatabaseSchemaEditor): | ||||
|     """Migrate permissions from OldAuthenticatedSession to AuthenticatedSession""" | ||||
|     db_alias = schema_editor.connection.alias | ||||
|  | ||||
|     # `apps` here is just an instance of `django.db.migrations.state.AppConfigStub`, we need the | ||||
|     # real config for creating permissions and content types | ||||
|     authentik_core_config = global_apps.get_app_config("authentik_core") | ||||
|     # These are only ran by django after all migrations, but we need them right now. | ||||
|     # `global_apps` is needed, | ||||
|     create_permissions(authentik_core_config, using=db_alias, verbosity=1) | ||||
|     create_contenttypes(authentik_core_config, using=db_alias, verbosity=1) | ||||
|  | ||||
|     # But from now on, this is just a regular migration, so use `apps` | ||||
|     Permission = apps.get_model("auth", "Permission") | ||||
|     ContentType = apps.get_model("contenttypes", "ContentType") | ||||
|  | ||||
|     try: | ||||
|         old_ct = ContentType.objects.using(db_alias).get( | ||||
|             app_label="authentik_core", model="oldauthenticatedsession" | ||||
|         ) | ||||
|         new_ct = ContentType.objects.using(db_alias).get( | ||||
|             app_label="authentik_core", model="authenticatedsession" | ||||
|         ) | ||||
|     except ContentType.DoesNotExist: | ||||
|         # This should exist at this point, but if not, let's cut our losses | ||||
|         return | ||||
|  | ||||
|     # Get all permissions for the old content type | ||||
|     old_perms = Permission.objects.using(db_alias).filter(content_type=old_ct) | ||||
|  | ||||
|     # Create equivalent permissions for the new content type | ||||
|     for old_perm in old_perms: | ||||
|         new_perm = ( | ||||
|             Permission.objects.using(db_alias) | ||||
|             .filter( | ||||
|                 content_type=new_ct, | ||||
|                 codename=old_perm.codename, | ||||
|             ) | ||||
|             .first() | ||||
|         ) | ||||
|         if not new_perm: | ||||
|             # This should exist at this point, but if not, let's cut our losses | ||||
|             continue | ||||
|  | ||||
|         # Global user permissions | ||||
|         User = apps.get_model("authentik_core", "User") | ||||
|         User.user_permissions.through.objects.using(db_alias).filter( | ||||
|             permission=old_perm | ||||
|         ).all().update(permission=new_perm) | ||||
|  | ||||
|         # Global role permissions | ||||
|         DjangoGroup = apps.get_model("auth", "Group") | ||||
|         DjangoGroup.permissions.through.objects.using(db_alias).filter( | ||||
|             permission=old_perm | ||||
|         ).all().update(permission=new_perm) | ||||
|  | ||||
|         # Object user permissions | ||||
|         UserObjectPermission = apps.get_model("guardian", "UserObjectPermission") | ||||
|         UserObjectPermission.objects.using(db_alias).filter(permission=old_perm).all().update( | ||||
|             permission=new_perm, content_type=new_ct | ||||
|         ) | ||||
|  | ||||
|         # Object role permissions | ||||
|         GroupObjectPermission = apps.get_model("guardian", "GroupObjectPermission") | ||||
|         GroupObjectPermission.objects.using(db_alias).filter(permission=old_perm).all().update( | ||||
|             permission=new_perm, content_type=new_ct | ||||
|         ) | ||||
|  | ||||
|  | ||||
| def remove_old_authenticated_session_content_type( | ||||
|     apps: Apps, schema_editor: BaseDatabaseSchemaEditor | ||||
| ): | ||||
| @ -21,7 +92,12 @@ class Migration(migrations.Migration): | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.RunPython( | ||||
|             code=migrate_authenticated_session_permissions, | ||||
|             reverse_code=migrations.RunPython.noop, | ||||
|         ), | ||||
|         migrations.RunPython( | ||||
|             code=remove_old_authenticated_session_content_type, | ||||
|             reverse_code=migrations.RunPython.noop, | ||||
|         ), | ||||
|     ] | ||||
|  | ||||
| @ -16,7 +16,7 @@ | ||||
|         {% block head_before %} | ||||
|         {% endblock %} | ||||
|         <link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}"> | ||||
|         <style>{{ brand.branding_custom_css }}</style> | ||||
|         <style>{{ brand_css }}</style> | ||||
|         <script src="{% versioned_script 'dist/poly-%v.js' %}" type="module"></script> | ||||
|         <script src="{% versioned_script 'dist/standalone/loading/index-%v.js' %}" type="module"></script> | ||||
|         {% block head %} | ||||
|  | ||||
| @ -15,13 +15,13 @@ class MMDBContextProcessor(EventContextProcessor): | ||||
|         self.reader: Reader | None = None | ||||
|         self._last_mtime: float = 0.0 | ||||
|         self.logger = get_logger() | ||||
|         self.open() | ||||
|         self.load() | ||||
|  | ||||
|     def path(self) -> str | None: | ||||
|         """Get the path to the MMDB file to load""" | ||||
|         raise NotImplementedError | ||||
|  | ||||
|     def open(self): | ||||
|     def load(self): | ||||
|         """Get GeoIP Reader, if configured, otherwise none""" | ||||
|         path = self.path() | ||||
|         if path == "" or not path: | ||||
| @ -44,7 +44,7 @@ class MMDBContextProcessor(EventContextProcessor): | ||||
|             diff = self._last_mtime < mtime | ||||
|             if diff > 0: | ||||
|                 self.logger.info("Found new MMDB Database, reopening", diff=diff, path=path) | ||||
|                 self.open() | ||||
|                 self.load() | ||||
|         except OSError as exc: | ||||
|             self.logger.warning("Failed to check MMDB age", exc=exc) | ||||
|  | ||||
|  | ||||
| @ -0,0 +1,18 @@ | ||||
| # Generated by Django 5.1.9 on 2025-05-27 12:52 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ("authentik_flows", "0027_auto_20231028_1424"), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name="flowtoken", | ||||
|             name="revoke_on_execution", | ||||
|             field=models.BooleanField(default=True), | ||||
|         ), | ||||
|     ] | ||||
| @ -303,9 +303,10 @@ class FlowToken(Token): | ||||
|  | ||||
|     flow = models.ForeignKey(Flow, on_delete=models.CASCADE) | ||||
|     _plan = models.TextField() | ||||
|     revoke_on_execution = models.BooleanField(default=True) | ||||
|  | ||||
|     @staticmethod | ||||
|     def pickle(plan) -> str: | ||||
|     def pickle(plan: "FlowPlan") -> str: | ||||
|         """Pickle into string""" | ||||
|         data = dumps(plan) | ||||
|         return b64encode(data).decode() | ||||
|  | ||||
| @ -99,9 +99,10 @@ class ChallengeStageView(StageView): | ||||
|             self.logger.debug("Got StageInvalidException", exc=exc) | ||||
|             return self.executor.stage_invalid() | ||||
|         if not challenge.is_valid(): | ||||
|             self.logger.warning( | ||||
|             self.logger.error( | ||||
|                 "f(ch): Invalid challenge", | ||||
|                 errors=challenge.errors, | ||||
|                 challenge=challenge.data, | ||||
|             ) | ||||
|         return HttpChallengeResponse(challenge) | ||||
|  | ||||
|  | ||||
| @ -146,6 +146,7 @@ class FlowExecutorView(APIView): | ||||
|         except (AttributeError, EOFError, ImportError, IndexError) as exc: | ||||
|             LOGGER.warning("f(exec): Failed to restore token plan", exc=exc) | ||||
|         finally: | ||||
|             if token.revoke_on_execution: | ||||
|                 token.delete() | ||||
|         if not isinstance(plan, FlowPlan): | ||||
|             return None | ||||
|  | ||||
| @ -81,7 +81,6 @@ debugger: false | ||||
|  | ||||
| log_level: info | ||||
|  | ||||
| session_storage: cache | ||||
| sessions: | ||||
|   unauthenticated_age: days=1 | ||||
|  | ||||
|  | ||||
| @ -1,6 +1,7 @@ | ||||
| from collections.abc import Callable | ||||
| from dataclasses import asdict | ||||
|  | ||||
| from celery import group | ||||
| from celery.exceptions import Retry | ||||
| from celery.result import allow_join_result | ||||
| from django.core.paginator import Paginator | ||||
| @ -82,21 +83,41 @@ class SyncTasks: | ||||
|                 self.logger.debug("Failed to acquire sync lock, skipping", provider=provider.name) | ||||
|                 return | ||||
|             try: | ||||
|                 for page in users_paginator.page_range: | ||||
|                     messages.append(_("Syncing page {page} of users".format(page=page))) | ||||
|                     for msg in sync_objects.apply_async( | ||||
|                 messages.append(_("Syncing users")) | ||||
|                 user_results = ( | ||||
|                     group( | ||||
|                         [ | ||||
|                             sync_objects.signature( | ||||
|                                 args=(class_to_path(User), page, provider_pk), | ||||
|                                 time_limit=PAGE_TIMEOUT, | ||||
|                                 soft_time_limit=PAGE_TIMEOUT, | ||||
|                     ).get(): | ||||
|                             ) | ||||
|                             for page in users_paginator.page_range | ||||
|                         ] | ||||
|                     ) | ||||
|                     .apply_async() | ||||
|                     .get() | ||||
|                 ) | ||||
|                 for result in user_results: | ||||
|                     for msg in result: | ||||
|                         messages.append(LogEvent(**msg)) | ||||
|                 for page in groups_paginator.page_range: | ||||
|                     messages.append(_("Syncing page {page} of groups".format(page=page))) | ||||
|                     for msg in sync_objects.apply_async( | ||||
|                 messages.append(_("Syncing groups")) | ||||
|                 group_results = ( | ||||
|                     group( | ||||
|                         [ | ||||
|                             sync_objects.signature( | ||||
|                                 args=(class_to_path(Group), page, provider_pk), | ||||
|                                 time_limit=PAGE_TIMEOUT, | ||||
|                                 soft_time_limit=PAGE_TIMEOUT, | ||||
|                     ).get(): | ||||
|                             ) | ||||
|                             for page in groups_paginator.page_range | ||||
|                         ] | ||||
|                     ) | ||||
|                     .apply_async() | ||||
|                     .get() | ||||
|                 ) | ||||
|                 for result in group_results: | ||||
|                     for msg in result: | ||||
|                         messages.append(LogEvent(**msg)) | ||||
|             except TransientSyncException as exc: | ||||
|                 self.logger.warning("transient sync exception", exc=exc) | ||||
| @ -109,7 +130,7 @@ class SyncTasks: | ||||
|     def sync_objects( | ||||
|         self, object_type: str, page: int, provider_pk: int, override_dry_run=False, **filter | ||||
|     ): | ||||
|         _object_type = path_to_class(object_type) | ||||
|         _object_type: type[Model] = path_to_class(object_type) | ||||
|         self.logger = get_logger().bind( | ||||
|             provider_type=class_to_path(self._provider_model), | ||||
|             provider_pk=provider_pk, | ||||
| @ -132,6 +153,19 @@ class SyncTasks: | ||||
|             self.logger.debug("starting discover") | ||||
|             client.discover() | ||||
|         self.logger.debug("starting sync for page", page=page) | ||||
|         messages.append( | ||||
|             asdict( | ||||
|                 LogEvent( | ||||
|                     _( | ||||
|                         "Syncing page {page} of {object_type}".format( | ||||
|                             page=page, object_type=_object_type._meta.verbose_name_plural | ||||
|                         ) | ||||
|                     ), | ||||
|                     log_level="info", | ||||
|                     logger=f"{provider._meta.verbose_name}@{object_type}", | ||||
|                 ) | ||||
|             ) | ||||
|         ) | ||||
|         for obj in paginator.page(page).object_list: | ||||
|             obj: Model | ||||
|             try: | ||||
|  | ||||
| @ -1,11 +1,9 @@ | ||||
| """Websocket tests""" | ||||
|  | ||||
| from dataclasses import asdict | ||||
| from unittest.mock import patch | ||||
|  | ||||
| from channels.routing import URLRouter | ||||
| from channels.testing import WebsocketCommunicator | ||||
| from django.contrib.contenttypes.models import ContentType | ||||
| from django.test import TransactionTestCase | ||||
|  | ||||
| from authentik import __version__ | ||||
| @ -16,12 +14,6 @@ from authentik.providers.proxy.models import ProxyProvider | ||||
| from authentik.root import websocket | ||||
|  | ||||
|  | ||||
| def patched__get_ct_cached(app_label, codename): | ||||
|     """Caches `ContentType` instances like its `QuerySet` does.""" | ||||
|     return ContentType.objects.get(app_label=app_label, permission__codename=codename) | ||||
|  | ||||
|  | ||||
| @patch("guardian.shortcuts._get_ct_cached", patched__get_ct_cached) | ||||
| class TestOutpostWS(TransactionTestCase): | ||||
|     """Websocket tests""" | ||||
|  | ||||
|  | ||||
| @ -66,7 +66,10 @@ class RACClientConsumer(AsyncWebsocketConsumer): | ||||
|     def init_outpost_connection(self): | ||||
|         """Initialize guac connection settings""" | ||||
|         self.token = ( | ||||
|             ConnectionToken.filter_not_expired(token=self.scope["url_route"]["kwargs"]["token"]) | ||||
|             ConnectionToken.filter_not_expired( | ||||
|                 token=self.scope["url_route"]["kwargs"]["token"], | ||||
|                 session__session__session_key=self.scope["session"].session_key, | ||||
|             ) | ||||
|             .select_related("endpoint", "provider", "session", "session__user") | ||||
|             .first() | ||||
|         ) | ||||
|  | ||||
| @ -166,7 +166,6 @@ class ConnectionToken(ExpiringModel): | ||||
|         always_merger.merge(settings, default_settings) | ||||
|         always_merger.merge(settings, self.endpoint.provider.settings) | ||||
|         always_merger.merge(settings, self.endpoint.settings) | ||||
|         always_merger.merge(settings, self.settings) | ||||
|  | ||||
|         def mapping_evaluator(mappings: QuerySet): | ||||
|             for mapping in mappings: | ||||
| @ -191,6 +190,7 @@ class ConnectionToken(ExpiringModel): | ||||
|         mapping_evaluator( | ||||
|             RACPropertyMapping.objects.filter(endpoint__in=[self.endpoint]).order_by("name") | ||||
|         ) | ||||
|         always_merger.merge(settings, self.settings) | ||||
|  | ||||
|         settings["drive-path"] = f"/tmp/connection/{self.token}"  # nosec | ||||
|         settings["create-drive-path"] = "true" | ||||
|  | ||||
| @ -90,23 +90,6 @@ class TestModels(TransactionTestCase): | ||||
|                 "resize-method": "display-update", | ||||
|             }, | ||||
|         ) | ||||
|         # Set settings in token | ||||
|         token.settings = { | ||||
|             "level": "token", | ||||
|         } | ||||
|         token.save() | ||||
|         self.assertEqual( | ||||
|             token.get_settings(), | ||||
|             { | ||||
|                 "hostname": self.endpoint.host.split(":")[0], | ||||
|                 "port": "1324", | ||||
|                 "client-name": f"authentik - {self.user}", | ||||
|                 "drive-path": path, | ||||
|                 "create-drive-path": "true", | ||||
|                 "level": "token", | ||||
|                 "resize-method": "display-update", | ||||
|             }, | ||||
|         ) | ||||
|         # Set settings in property mapping (provider) | ||||
|         mapping = RACPropertyMapping.objects.create( | ||||
|             name=generate_id(), | ||||
| @ -151,3 +134,22 @@ class TestModels(TransactionTestCase): | ||||
|                 "resize-method": "display-update", | ||||
|             }, | ||||
|         ) | ||||
|         # Set settings in token | ||||
|         token.settings = { | ||||
|             "level": "token", | ||||
|         } | ||||
|         token.save() | ||||
|         self.assertEqual( | ||||
|             token.get_settings(), | ||||
|             { | ||||
|                 "hostname": self.endpoint.host.split(":")[0], | ||||
|                 "port": "1324", | ||||
|                 "client-name": f"authentik - {self.user}", | ||||
|                 "drive-path": path, | ||||
|                 "create-drive-path": "true", | ||||
|                 "foo": "true", | ||||
|                 "bar": "6", | ||||
|                 "resize-method": "display-update", | ||||
|                 "level": "token", | ||||
|             }, | ||||
|         ) | ||||
|  | ||||
| @ -87,3 +87,22 @@ class TestRACViews(APITestCase): | ||||
|         ) | ||||
|         body = loads(flow_response.content) | ||||
|         self.assertEqual(body["component"], "ak-stage-access-denied") | ||||
|  | ||||
|     def test_different_session(self): | ||||
|         """Test request""" | ||||
|         self.client.force_login(self.user) | ||||
|         response = self.client.get( | ||||
|             reverse( | ||||
|                 "authentik_providers_rac:start", | ||||
|                 kwargs={"app": self.app.slug, "endpoint": str(self.endpoint.pk)}, | ||||
|             ) | ||||
|         ) | ||||
|         self.assertEqual(response.status_code, 302) | ||||
|         flow_response = self.client.get( | ||||
|             reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}) | ||||
|         ) | ||||
|         body = loads(flow_response.content) | ||||
|         next_url = body["to"] | ||||
|         self.client.logout() | ||||
|         final_response = self.client.get(next_url) | ||||
|         self.assertEqual(final_response.url, reverse("authentik_core:if-user")) | ||||
|  | ||||
| @ -20,6 +20,9 @@ from authentik.lib.utils.time import timedelta_from_string | ||||
| from authentik.policies.engine import PolicyEngine | ||||
| from authentik.policies.views import PolicyAccessView | ||||
| from authentik.providers.rac.models import ConnectionToken, Endpoint, RACProvider | ||||
| from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT | ||||
|  | ||||
| PLAN_CONNECTION_SETTINGS = "connection_settings" | ||||
|  | ||||
|  | ||||
| class RACStartView(PolicyAccessView): | ||||
| @ -65,7 +68,10 @@ class RACInterface(InterfaceView): | ||||
|  | ||||
|     def dispatch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse: | ||||
|         # Early sanity check to ensure token still exists | ||||
|         token = ConnectionToken.filter_not_expired(token=self.kwargs["token"]).first() | ||||
|         token = ConnectionToken.filter_not_expired( | ||||
|             token=self.kwargs["token"], | ||||
|             session__session__session_key=request.session.session_key, | ||||
|         ).first() | ||||
|         if not token: | ||||
|             return redirect("authentik_core:if-user") | ||||
|         self.token = token | ||||
| @ -109,10 +115,15 @@ class RACFinalStage(RedirectStage): | ||||
|         return super().dispatch(request, *args, **kwargs) | ||||
|  | ||||
|     def get_challenge(self, *args, **kwargs) -> RedirectChallenge: | ||||
|         settings = self.executor.plan.context.get(PLAN_CONNECTION_SETTINGS) | ||||
|         if not settings: | ||||
|             settings = self.executor.plan.context.get(PLAN_CONTEXT_PROMPT, {}).get( | ||||
|                 PLAN_CONNECTION_SETTINGS | ||||
|             ) | ||||
|         token = ConnectionToken.objects.create( | ||||
|             provider=self.provider, | ||||
|             endpoint=self.endpoint, | ||||
|             settings=self.executor.plan.context.get("connection_settings", {}), | ||||
|             settings=settings or {}, | ||||
|             session=self.request.session["authenticatedsession"], | ||||
|             expires=now() + timedelta_from_string(self.provider.connection_expiry), | ||||
|             expiring=True, | ||||
|  | ||||
| @ -47,15 +47,16 @@ class SCIMGroupClient(SCIMClient[Group, SCIMProviderGroup, SCIMGroupSchema]): | ||||
|  | ||||
|     def to_schema(self, obj: Group, connection: SCIMProviderGroup) -> SCIMGroupSchema: | ||||
|         """Convert authentik user into SCIM""" | ||||
|         raw_scim_group = super().to_schema( | ||||
|             obj, | ||||
|             connection, | ||||
|             schemas=(SCIM_GROUP_SCHEMA,), | ||||
|         ) | ||||
|         raw_scim_group = super().to_schema(obj, connection) | ||||
|         try: | ||||
|             scim_group = SCIMGroupSchema.model_validate(delete_none_values(raw_scim_group)) | ||||
|         except ValidationError as exc: | ||||
|             raise StopSync(exc, obj) from exc | ||||
|         if SCIM_GROUP_SCHEMA not in scim_group.schemas: | ||||
|             scim_group.schemas.insert(0, SCIM_GROUP_SCHEMA) | ||||
|         # As this might be unset, we need to tell pydantic it's set so ensure the schemas | ||||
|         # are included, even if its just the defaults | ||||
|         scim_group.schemas = list(scim_group.schemas) | ||||
|         if not scim_group.externalId: | ||||
|             scim_group.externalId = str(obj.pk) | ||||
|  | ||||
|  | ||||
| @ -31,15 +31,16 @@ class SCIMUserClient(SCIMClient[User, SCIMProviderUser, SCIMUserSchema]): | ||||
|  | ||||
|     def to_schema(self, obj: User, connection: SCIMProviderUser) -> SCIMUserSchema: | ||||
|         """Convert authentik user into SCIM""" | ||||
|         raw_scim_user = super().to_schema( | ||||
|             obj, | ||||
|             connection, | ||||
|             schemas=(SCIM_USER_SCHEMA,), | ||||
|         ) | ||||
|         raw_scim_user = super().to_schema(obj, connection) | ||||
|         try: | ||||
|             scim_user = SCIMUserSchema.model_validate(delete_none_values(raw_scim_user)) | ||||
|         except ValidationError as exc: | ||||
|             raise StopSync(exc, obj) from exc | ||||
|         if SCIM_USER_SCHEMA not in scim_user.schemas: | ||||
|             scim_user.schemas.insert(0, SCIM_USER_SCHEMA) | ||||
|         # As this might be unset, we need to tell pydantic it's set so ensure the schemas | ||||
|         # are included, even if its just the defaults | ||||
|         scim_user.schemas = list(scim_user.schemas) | ||||
|         if not scim_user.externalId: | ||||
|             scim_user.externalId = str(obj.uid) | ||||
|         return scim_user | ||||
|  | ||||
| @ -91,6 +91,57 @@ class SCIMUserTests(TestCase): | ||||
|             }, | ||||
|         ) | ||||
|  | ||||
|     @Mocker() | ||||
|     def test_user_create_custom_schema(self, mock: Mocker): | ||||
|         """Test user creation with custom schema""" | ||||
|         schema = SCIMMapping.objects.create( | ||||
|             name="custom_schema", | ||||
|             expression="""return {"schemas": ["foo"]}""", | ||||
|         ) | ||||
|         self.provider.property_mappings.add(schema) | ||||
|         scim_id = generate_id() | ||||
|         mock.get( | ||||
|             "https://localhost/ServiceProviderConfig", | ||||
|             json={}, | ||||
|         ) | ||||
|         mock.post( | ||||
|             "https://localhost/Users", | ||||
|             json={ | ||||
|                 "id": scim_id, | ||||
|             }, | ||||
|         ) | ||||
|         uid = generate_id() | ||||
|         user = User.objects.create( | ||||
|             username=uid, | ||||
|             name=f"{uid} {uid}", | ||||
|             email=f"{uid}@goauthentik.io", | ||||
|         ) | ||||
|         self.assertEqual(mock.call_count, 2) | ||||
|         self.assertEqual(mock.request_history[0].method, "GET") | ||||
|         self.assertEqual(mock.request_history[1].method, "POST") | ||||
|         self.assertJSONEqual( | ||||
|             mock.request_history[1].body, | ||||
|             { | ||||
|                 "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User", "foo"], | ||||
|                 "active": True, | ||||
|                 "emails": [ | ||||
|                     { | ||||
|                         "primary": True, | ||||
|                         "type": "other", | ||||
|                         "value": f"{uid}@goauthentik.io", | ||||
|                     } | ||||
|                 ], | ||||
|                 "externalId": user.uid, | ||||
|                 "name": { | ||||
|                     "familyName": uid, | ||||
|                     "formatted": f"{uid} {uid}", | ||||
|                     "givenName": uid, | ||||
|                 }, | ||||
|                 "displayName": f"{uid} {uid}", | ||||
|                 "userName": uid, | ||||
|             }, | ||||
|         ) | ||||
|  | ||||
|     @Mocker() | ||||
|     def test_user_create_different_provider_same_id(self, mock: Mocker): | ||||
|         """Test user creation with multiple providers that happen | ||||
| @ -384,7 +435,7 @@ class SCIMUserTests(TestCase): | ||||
|                 self.assertIn(request.method, SAFE_METHODS) | ||||
|         task = SystemTask.objects.filter(uid=slugify(self.provider.name)).first() | ||||
|         self.assertIsNotNone(task) | ||||
|         drop_msg = task.messages[2] | ||||
|         drop_msg = task.messages[3] | ||||
|         self.assertEqual(drop_msg["event"], "Dropping mutating request due to dry run") | ||||
|         self.assertIsNotNone(drop_msg["attributes"]["url"]) | ||||
|         self.assertIsNotNone(drop_msg["attributes"]["body"]) | ||||
|  | ||||
| @ -424,7 +424,7 @@ else: | ||||
|         "BACKEND": "authentik.root.storages.FileStorage", | ||||
|         "OPTIONS": { | ||||
|             "location": Path(CONFIG.get("storage.media.file.path")), | ||||
|             "base_url": "/media/", | ||||
|             "base_url": CONFIG.get("web.path", "/") + "media/", | ||||
|         }, | ||||
|     } | ||||
|     # Compatibility for apps not supporting top-level STORAGES | ||||
|  | ||||
| @ -3,25 +3,46 @@ | ||||
| import os | ||||
| from argparse import ArgumentParser | ||||
| from unittest import TestCase | ||||
| from unittest.mock import patch | ||||
|  | ||||
| import pytest | ||||
| from django.conf import settings | ||||
| from django.contrib.contenttypes.models import ContentType | ||||
| from django.test.runner import DiscoverRunner | ||||
| from structlog.stdlib import get_logger | ||||
|  | ||||
| from authentik.events.context_processors.asn import ASN_CONTEXT_PROCESSOR | ||||
| from authentik.events.context_processors.geoip import GEOIP_CONTEXT_PROCESSOR | ||||
| from authentik.lib.config import CONFIG | ||||
| from authentik.lib.sentry import sentry_init | ||||
| from authentik.root.signals import post_startup, pre_startup, startup | ||||
| from tests.e2e.utils import get_docker_tag | ||||
|  | ||||
| # globally set maxDiff to none to show full assert error | ||||
| TestCase.maxDiff = None | ||||
|  | ||||
|  | ||||
| def get_docker_tag() -> str: | ||||
|     """Get docker-tag based off of CI variables""" | ||||
|     env_pr_branch = "GITHUB_HEAD_REF" | ||||
|     default_branch = "GITHUB_REF" | ||||
|     branch_name = os.environ.get(default_branch, "main") | ||||
|     if os.environ.get(env_pr_branch, "") != "": | ||||
|         branch_name = os.environ[env_pr_branch] | ||||
|     branch_name = branch_name.replace("refs/heads/", "").replace("/", "-") | ||||
|     return f"gh-{branch_name}" | ||||
|  | ||||
|  | ||||
| def patched__get_ct_cached(app_label, codename): | ||||
|     """Caches `ContentType` instances like its `QuerySet` does.""" | ||||
|     return ContentType.objects.get(app_label=app_label, permission__codename=codename) | ||||
|  | ||||
|  | ||||
| class PytestTestRunner(DiscoverRunner):  # pragma: no cover | ||||
|     """Runs pytest to discover and run tests.""" | ||||
|  | ||||
|     def __init__(self, **kwargs): | ||||
|         super().__init__(**kwargs) | ||||
|         self.logger = get_logger().bind(runner="pytest") | ||||
|  | ||||
|         self.args = [] | ||||
|         if self.failfast: | ||||
| @ -31,6 +52,8 @@ class PytestTestRunner(DiscoverRunner):  # pragma: no cover | ||||
|  | ||||
|         if kwargs.get("randomly_seed", None): | ||||
|             self.args.append(f"--randomly-seed={kwargs['randomly_seed']}") | ||||
|         if kwargs.get("no_capture", False): | ||||
|             self.args.append("--capture=no") | ||||
|  | ||||
|         settings.TEST = True | ||||
|         settings.CELERY["task_always_eager"] = True | ||||
| @ -46,6 +69,10 @@ class PytestTestRunner(DiscoverRunner):  # pragma: no cover | ||||
|         CONFIG.set("error_reporting.sample_rate", 0) | ||||
|         CONFIG.set("error_reporting.environment", "testing") | ||||
|         CONFIG.set("error_reporting.send_pii", True) | ||||
|  | ||||
|         ASN_CONTEXT_PROCESSOR.load() | ||||
|         GEOIP_CONTEXT_PROCESSOR.load() | ||||
|  | ||||
|         sentry_init() | ||||
|  | ||||
|         pre_startup.send(sender=self, mode="test") | ||||
| @ -64,6 +91,11 @@ class PytestTestRunner(DiscoverRunner):  # pragma: no cover | ||||
|             "Default behaviour: use random.Random().getrandbits(32), so the seed is" | ||||
|             "different on each run.", | ||||
|         ) | ||||
|         parser.add_argument( | ||||
|             "--no-capture", | ||||
|             action="store_true", | ||||
|             help="Disable any capturing of stdout/stderr during tests.", | ||||
|         ) | ||||
|  | ||||
|     def run_tests(self, test_labels, extra_tests=None, **kwargs): | ||||
|         """Run pytest and return the exitcode. | ||||
| @ -106,4 +138,10 @@ class PytestTestRunner(DiscoverRunner):  # pragma: no cover | ||||
|                     f"path instead." | ||||
|                 ) | ||||
|  | ||||
|         self.logger.info("Running tests", test_files=self.args) | ||||
|         with patch("guardian.shortcuts._get_ct_cached", patched__get_ct_cached): | ||||
|             try: | ||||
|                 return pytest.main(self.args) | ||||
|             except Exception as e: | ||||
|                 self.logger.error("Error running tests", error=str(e), test_files=self.args) | ||||
|                 return 1 | ||||
|  | ||||
| @ -103,6 +103,7 @@ class LDAPSourceSerializer(SourceSerializer): | ||||
|             "user_object_filter", | ||||
|             "group_object_filter", | ||||
|             "group_membership_field", | ||||
|             "user_membership_attribute", | ||||
|             "object_uniqueness_field", | ||||
|             "password_login_update_internal_password", | ||||
|             "sync_users", | ||||
| @ -111,6 +112,7 @@ class LDAPSourceSerializer(SourceSerializer): | ||||
|             "sync_parent_group", | ||||
|             "connectivity", | ||||
|             "lookup_groups_from_user", | ||||
|             "delete_not_found_objects", | ||||
|         ] | ||||
|         extra_kwargs = {"bind_password": {"write_only": True}} | ||||
|  | ||||
| @ -138,6 +140,7 @@ class LDAPSourceViewSet(UsedByMixin, ModelViewSet): | ||||
|         "user_object_filter", | ||||
|         "group_object_filter", | ||||
|         "group_membership_field", | ||||
|         "user_membership_attribute", | ||||
|         "object_uniqueness_field", | ||||
|         "password_login_update_internal_password", | ||||
|         "sync_users", | ||||
| @ -147,6 +150,7 @@ class LDAPSourceViewSet(UsedByMixin, ModelViewSet): | ||||
|         "user_property_mappings", | ||||
|         "group_property_mappings", | ||||
|         "lookup_groups_from_user", | ||||
|         "delete_not_found_objects", | ||||
|     ] | ||||
|     search_fields = ["name", "slug"] | ||||
|     ordering = ["name"] | ||||
|  | ||||
| @ -0,0 +1,48 @@ | ||||
| # Generated by Django 5.1.9 on 2025-05-28 08:15 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ("authentik_core", "0048_delete_oldauthenticatedsession_content_type"), | ||||
|         ("authentik_sources_ldap", "0008_groupldapsourceconnection_userldapsourceconnection"), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name="groupldapsourceconnection", | ||||
|             name="validated_by", | ||||
|             field=models.UUIDField( | ||||
|                 blank=True, | ||||
|                 help_text="Unique ID used while checking if this object still exists in the directory.", | ||||
|                 null=True, | ||||
|             ), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name="ldapsource", | ||||
|             name="delete_not_found_objects", | ||||
|             field=models.BooleanField( | ||||
|                 default=False, | ||||
|                 help_text="Delete authentik users and groups which were previously supplied by this source, but are now missing from it.", | ||||
|             ), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name="userldapsourceconnection", | ||||
|             name="validated_by", | ||||
|             field=models.UUIDField( | ||||
|                 blank=True, | ||||
|                 help_text="Unique ID used while checking if this object still exists in the directory.", | ||||
|                 null=True, | ||||
|             ), | ||||
|         ), | ||||
|         migrations.AddIndex( | ||||
|             model_name="groupldapsourceconnection", | ||||
|             index=models.Index(fields=["validated_by"], name="authentik_s_validat_b70447_idx"), | ||||
|         ), | ||||
|         migrations.AddIndex( | ||||
|             model_name="userldapsourceconnection", | ||||
|             index=models.Index(fields=["validated_by"], name="authentik_s_validat_ff2ebc_idx"), | ||||
|         ), | ||||
|     ] | ||||
| @ -0,0 +1,32 @@ | ||||
| # Generated by Django 5.1.9 on 2025-05-29 11:22 | ||||
|  | ||||
| from django.apps.registry import Apps | ||||
| from django.db import migrations, models | ||||
| from django.db.backends.base.schema import BaseDatabaseSchemaEditor | ||||
|  | ||||
|  | ||||
| def set_user_membership_attribute(apps: Apps, schema_editor: BaseDatabaseSchemaEditor): | ||||
|     LDAPSource = apps.get_model("authentik_sources_ldap", "LDAPSource") | ||||
|     db_alias = schema_editor.connection.alias | ||||
|  | ||||
|     LDAPSource.objects.using(db_alias).filter(group_membership_field="memberUid").all().update( | ||||
|         user_membership_attribute="ldap_uniq" | ||||
|     ) | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|     dependencies = [ | ||||
|         ("authentik_sources_ldap", "0009_groupldapsourceconnection_validated_by_and_more"), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name="ldapsource", | ||||
|             name="user_membership_attribute", | ||||
|             field=models.TextField( | ||||
|                 default="distinguishedName", | ||||
|                 help_text="Attribute which matches the value of `group_membership_field`.", | ||||
|             ), | ||||
|         ), | ||||
|         migrations.RunPython(set_user_membership_attribute, migrations.RunPython.noop), | ||||
|     ] | ||||
| @ -100,6 +100,10 @@ class LDAPSource(Source): | ||||
|         default="(objectClass=person)", | ||||
|         help_text=_("Consider Objects matching this filter to be Users."), | ||||
|     ) | ||||
|     user_membership_attribute = models.TextField( | ||||
|         default=LDAP_DISTINGUISHED_NAME, | ||||
|         help_text=_("Attribute which matches the value of `group_membership_field`."), | ||||
|     ) | ||||
|     group_membership_field = models.TextField( | ||||
|         default="member", help_text=_("Field which contains members of a group.") | ||||
|     ) | ||||
| @ -137,6 +141,14 @@ class LDAPSource(Source): | ||||
|         ), | ||||
|     ) | ||||
|  | ||||
|     delete_not_found_objects = models.BooleanField( | ||||
|         default=False, | ||||
|         help_text=_( | ||||
|             "Delete authentik users and groups which were previously supplied by this source, " | ||||
|             "but are now missing from it." | ||||
|         ), | ||||
|     ) | ||||
|  | ||||
|     @property | ||||
|     def component(self) -> str: | ||||
|         return "ak-source-ldap-form" | ||||
| @ -321,6 +333,12 @@ class LDAPSourcePropertyMapping(PropertyMapping): | ||||
|  | ||||
|  | ||||
| class UserLDAPSourceConnection(UserSourceConnection): | ||||
|     validated_by = models.UUIDField( | ||||
|         null=True, | ||||
|         blank=True, | ||||
|         help_text=_("Unique ID used while checking if this object still exists in the directory."), | ||||
|     ) | ||||
|  | ||||
|     @property | ||||
|     def serializer(self) -> type[Serializer]: | ||||
|         from authentik.sources.ldap.api import ( | ||||
| @ -332,9 +350,18 @@ class UserLDAPSourceConnection(UserSourceConnection): | ||||
|     class Meta: | ||||
|         verbose_name = _("User LDAP Source Connection") | ||||
|         verbose_name_plural = _("User LDAP Source Connections") | ||||
|         indexes = [ | ||||
|             models.Index(fields=["validated_by"]), | ||||
|         ] | ||||
|  | ||||
|  | ||||
| class GroupLDAPSourceConnection(GroupSourceConnection): | ||||
|     validated_by = models.UUIDField( | ||||
|         null=True, | ||||
|         blank=True, | ||||
|         help_text=_("Unique ID used while checking if this object still exists in the directory."), | ||||
|     ) | ||||
|  | ||||
|     @property | ||||
|     def serializer(self) -> type[Serializer]: | ||||
|         from authentik.sources.ldap.api import ( | ||||
| @ -346,3 +373,6 @@ class GroupLDAPSourceConnection(GroupSourceConnection): | ||||
|     class Meta: | ||||
|         verbose_name = _("Group LDAP Source Connection") | ||||
|         verbose_name_plural = _("Group LDAP Source Connections") | ||||
|         indexes = [ | ||||
|             models.Index(fields=["validated_by"]), | ||||
|         ] | ||||
|  | ||||
| @ -9,7 +9,7 @@ from structlog.stdlib import BoundLogger, get_logger | ||||
| from authentik.core.sources.mapper import SourceMapper | ||||
| from authentik.lib.config import CONFIG | ||||
| from authentik.lib.sync.mapper import PropertyMappingManager | ||||
| from authentik.sources.ldap.models import LDAPSource | ||||
| from authentik.sources.ldap.models import LDAPSource, flatten | ||||
|  | ||||
|  | ||||
| class BaseLDAPSynchronizer: | ||||
| @ -77,6 +77,16 @@ class BaseLDAPSynchronizer: | ||||
|         """Get objects from LDAP, implemented in subclass""" | ||||
|         raise NotImplementedError() | ||||
|  | ||||
|     def get_attributes(self, object): | ||||
|         if "attributes" not in object: | ||||
|             return | ||||
|         return object.get("attributes", {}) | ||||
|  | ||||
|     def get_identifier(self, attributes: dict): | ||||
|         if not attributes.get(self._source.object_uniqueness_field): | ||||
|             return | ||||
|         return flatten(attributes[self._source.object_uniqueness_field]) | ||||
|  | ||||
|     def search_paginator(  # noqa: PLR0913 | ||||
|         self, | ||||
|         search_base, | ||||
|  | ||||
							
								
								
									
										61
									
								
								authentik/sources/ldap/sync/forward_delete_groups.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								authentik/sources/ldap/sync/forward_delete_groups.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,61 @@ | ||||
| from collections.abc import Generator | ||||
| from itertools import batched | ||||
| from uuid import uuid4 | ||||
|  | ||||
| from ldap3 import SUBTREE | ||||
|  | ||||
| from authentik.core.models import Group | ||||
| from authentik.sources.ldap.models import GroupLDAPSourceConnection | ||||
| from authentik.sources.ldap.sync.base import BaseLDAPSynchronizer | ||||
| from authentik.sources.ldap.sync.forward_delete_users import DELETE_CHUNK_SIZE, UPDATE_CHUNK_SIZE | ||||
|  | ||||
|  | ||||
| class GroupLDAPForwardDeletion(BaseLDAPSynchronizer): | ||||
|     """Delete LDAP Groups from authentik""" | ||||
|  | ||||
|     @staticmethod | ||||
|     def name() -> str: | ||||
|         return "group_deletions" | ||||
|  | ||||
|     def get_objects(self, **kwargs) -> Generator: | ||||
|         if not self._source.sync_groups or not self._source.delete_not_found_objects: | ||||
|             self.message("Group syncing is disabled for this Source") | ||||
|             return iter(()) | ||||
|  | ||||
|         uuid = uuid4() | ||||
|         groups = self._source.connection().extend.standard.paged_search( | ||||
|             search_base=self.base_dn_groups, | ||||
|             search_filter=self._source.group_object_filter, | ||||
|             search_scope=SUBTREE, | ||||
|             attributes=[self._source.object_uniqueness_field], | ||||
|             generator=True, | ||||
|             **kwargs, | ||||
|         ) | ||||
|         for batch in batched(groups, UPDATE_CHUNK_SIZE, strict=False): | ||||
|             identifiers = [] | ||||
|             for group in batch: | ||||
|                 if not (attributes := self.get_attributes(group)): | ||||
|                     continue | ||||
|                 if identifier := self.get_identifier(attributes): | ||||
|                     identifiers.append(identifier) | ||||
|             GroupLDAPSourceConnection.objects.filter(identifier__in=identifiers).update( | ||||
|                 validated_by=uuid | ||||
|             ) | ||||
|  | ||||
|         return batched( | ||||
|             GroupLDAPSourceConnection.objects.filter(source=self._source) | ||||
|             .exclude(validated_by=uuid) | ||||
|             .values_list("group", flat=True) | ||||
|             .iterator(chunk_size=DELETE_CHUNK_SIZE), | ||||
|             DELETE_CHUNK_SIZE, | ||||
|             strict=False, | ||||
|         ) | ||||
|  | ||||
|     def sync(self, group_pks: tuple) -> int: | ||||
|         """Delete authentik groups""" | ||||
|         if not self._source.sync_groups or not self._source.delete_not_found_objects: | ||||
|             self.message("Group syncing is disabled for this Source") | ||||
|             return -1 | ||||
|         self._logger.debug("Deleting groups", group_pks=group_pks) | ||||
|         _, deleted_per_type = Group.objects.filter(pk__in=group_pks).delete() | ||||
|         return deleted_per_type.get(Group._meta.label, 0) | ||||
							
								
								
									
										63
									
								
								authentik/sources/ldap/sync/forward_delete_users.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								authentik/sources/ldap/sync/forward_delete_users.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,63 @@ | ||||
| from collections.abc import Generator | ||||
| from itertools import batched | ||||
| from uuid import uuid4 | ||||
|  | ||||
| from ldap3 import SUBTREE | ||||
|  | ||||
| from authentik.core.models import User | ||||
| from authentik.sources.ldap.models import UserLDAPSourceConnection | ||||
| from authentik.sources.ldap.sync.base import BaseLDAPSynchronizer | ||||
|  | ||||
| UPDATE_CHUNK_SIZE = 10_000 | ||||
| DELETE_CHUNK_SIZE = 50 | ||||
|  | ||||
|  | ||||
| class UserLDAPForwardDeletion(BaseLDAPSynchronizer): | ||||
|     """Delete LDAP Users from authentik""" | ||||
|  | ||||
|     @staticmethod | ||||
|     def name() -> str: | ||||
|         return "user_deletions" | ||||
|  | ||||
|     def get_objects(self, **kwargs) -> Generator: | ||||
|         if not self._source.sync_users or not self._source.delete_not_found_objects: | ||||
|             self.message("User syncing is disabled for this Source") | ||||
|             return iter(()) | ||||
|  | ||||
|         uuid = uuid4() | ||||
|         users = self._source.connection().extend.standard.paged_search( | ||||
|             search_base=self.base_dn_users, | ||||
|             search_filter=self._source.user_object_filter, | ||||
|             search_scope=SUBTREE, | ||||
|             attributes=[self._source.object_uniqueness_field], | ||||
|             generator=True, | ||||
|             **kwargs, | ||||
|         ) | ||||
|         for batch in batched(users, UPDATE_CHUNK_SIZE, strict=False): | ||||
|             identifiers = [] | ||||
|             for user in batch: | ||||
|                 if not (attributes := self.get_attributes(user)): | ||||
|                     continue | ||||
|                 if identifier := self.get_identifier(attributes): | ||||
|                     identifiers.append(identifier) | ||||
|             UserLDAPSourceConnection.objects.filter(identifier__in=identifiers).update( | ||||
|                 validated_by=uuid | ||||
|             ) | ||||
|  | ||||
|         return batched( | ||||
|             UserLDAPSourceConnection.objects.filter(source=self._source) | ||||
|             .exclude(validated_by=uuid) | ||||
|             .values_list("user", flat=True) | ||||
|             .iterator(chunk_size=DELETE_CHUNK_SIZE), | ||||
|             DELETE_CHUNK_SIZE, | ||||
|             strict=False, | ||||
|         ) | ||||
|  | ||||
|     def sync(self, user_pks: tuple) -> int: | ||||
|         """Delete authentik users""" | ||||
|         if not self._source.sync_users or not self._source.delete_not_found_objects: | ||||
|             self.message("User syncing is disabled for this Source") | ||||
|             return -1 | ||||
|         self._logger.debug("Deleting users", user_pks=user_pks) | ||||
|         _, deleted_per_type = User.objects.filter(pk__in=user_pks).delete() | ||||
|         return deleted_per_type.get(User._meta.label, 0) | ||||
| @ -58,18 +58,16 @@ class GroupLDAPSynchronizer(BaseLDAPSynchronizer): | ||||
|             return -1 | ||||
|         group_count = 0 | ||||
|         for group in page_data: | ||||
|             if "attributes" not in group: | ||||
|             if (attributes := self.get_attributes(group)) is None: | ||||
|                 continue | ||||
|             attributes = group.get("attributes", {}) | ||||
|             group_dn = flatten(flatten(group.get("entryDN", group.get("dn")))) | ||||
|             if not attributes.get(self._source.object_uniqueness_field): | ||||
|             if not (uniq := self.get_identifier(attributes)): | ||||
|                 self.message( | ||||
|                     f"Uniqueness field not found/not set in attributes: '{group_dn}'", | ||||
|                     attributes=attributes.keys(), | ||||
|                     dn=group_dn, | ||||
|                 ) | ||||
|                 continue | ||||
|             uniq = flatten(attributes[self._source.object_uniqueness_field]) | ||||
|             try: | ||||
|                 defaults = { | ||||
|                     k: flatten(v) | ||||
|  | ||||
| @ -63,25 +63,19 @@ class MembershipLDAPSynchronizer(BaseLDAPSynchronizer): | ||||
|                     group_member_dn = group_member.get("dn", {}) | ||||
|                     members.append(group_member_dn) | ||||
|             else: | ||||
|                 if "attributes" not in group: | ||||
|                 if (attributes := self.get_attributes(group)) is None: | ||||
|                     continue | ||||
|                 members = group.get("attributes", {}).get(self._source.group_membership_field, []) | ||||
|                 members = attributes.get(self._source.group_membership_field, []) | ||||
|  | ||||
|             ak_group = self.get_group(group) | ||||
|             if not ak_group: | ||||
|                 continue | ||||
|  | ||||
|             membership_mapping_attribute = LDAP_DISTINGUISHED_NAME | ||||
|             if self._source.group_membership_field == "memberUid": | ||||
|                 # If memberships are based on the posixGroup's 'memberUid' | ||||
|                 # attribute we use the RDN instead of the FDN to lookup members. | ||||
|                 membership_mapping_attribute = LDAP_UNIQUENESS | ||||
|  | ||||
|             users = User.objects.filter( | ||||
|                 Q(**{f"attributes__{membership_mapping_attribute}__in": members}) | ||||
|                 Q(**{f"attributes__{self._source.user_membership_attribute}__in": members}) | ||||
|                 | Q( | ||||
|                     **{ | ||||
|                         f"attributes__{membership_mapping_attribute}__isnull": True, | ||||
|                         f"attributes__{self._source.user_membership_attribute}__isnull": True, | ||||
|                         "ak_groups__in": [ak_group], | ||||
|                     } | ||||
|                 ) | ||||
|  | ||||
| @ -60,18 +60,16 @@ class UserLDAPSynchronizer(BaseLDAPSynchronizer): | ||||
|             return -1 | ||||
|         user_count = 0 | ||||
|         for user in page_data: | ||||
|             if "attributes" not in user: | ||||
|             if (attributes := self.get_attributes(user)) is None: | ||||
|                 continue | ||||
|             attributes = user.get("attributes", {}) | ||||
|             user_dn = flatten(user.get("entryDN", user.get("dn"))) | ||||
|             if not attributes.get(self._source.object_uniqueness_field): | ||||
|             if not (uniq := self.get_identifier(attributes)): | ||||
|                 self.message( | ||||
|                     f"Uniqueness field not found/not set in attributes: '{user_dn}'", | ||||
|                     attributes=attributes.keys(), | ||||
|                     dn=user_dn, | ||||
|                 ) | ||||
|                 continue | ||||
|             uniq = flatten(attributes[self._source.object_uniqueness_field]) | ||||
|             try: | ||||
|                 defaults = { | ||||
|                     k: flatten(v) | ||||
|  | ||||
| @ -17,6 +17,8 @@ from authentik.lib.utils.reflection import class_to_path, path_to_class | ||||
| from authentik.root.celery import CELERY_APP | ||||
| from authentik.sources.ldap.models import LDAPSource | ||||
| from authentik.sources.ldap.sync.base import BaseLDAPSynchronizer | ||||
| from authentik.sources.ldap.sync.forward_delete_groups import GroupLDAPForwardDeletion | ||||
| from authentik.sources.ldap.sync.forward_delete_users import UserLDAPForwardDeletion | ||||
| from authentik.sources.ldap.sync.groups import GroupLDAPSynchronizer | ||||
| from authentik.sources.ldap.sync.membership import MembershipLDAPSynchronizer | ||||
| from authentik.sources.ldap.sync.users import UserLDAPSynchronizer | ||||
| @ -52,11 +54,11 @@ def ldap_connectivity_check(pk: str | None = None): | ||||
|  | ||||
|  | ||||
| @CELERY_APP.task( | ||||
|     # We take the configured hours timeout time by 2.5 as we run user and | ||||
|     # group in parallel and then membership, so 2x is to cover the serial tasks, | ||||
|     # We take the configured hours timeout time by 3.5 as we run user and | ||||
|     # group in parallel and then membership, then deletions, so 3x is to cover the serial tasks, | ||||
|     # and 0.5x on top of that to give some more leeway | ||||
|     soft_time_limit=(60 * 60 * CONFIG.get_int("ldap.task_timeout_hours")) * 2.5, | ||||
|     task_time_limit=(60 * 60 * CONFIG.get_int("ldap.task_timeout_hours")) * 2.5, | ||||
|     soft_time_limit=(60 * 60 * CONFIG.get_int("ldap.task_timeout_hours")) * 3.5, | ||||
|     task_time_limit=(60 * 60 * CONFIG.get_int("ldap.task_timeout_hours")) * 3.5, | ||||
| ) | ||||
| def ldap_sync_single(source_pk: str): | ||||
|     """Sync a single source""" | ||||
| @ -69,18 +71,31 @@ def ldap_sync_single(source_pk: str): | ||||
|             return | ||||
|         # Delete all sync tasks from the cache | ||||
|         DBSystemTask.objects.filter(name="ldap_sync", uid__startswith=source.slug).delete() | ||||
|         task = chain( | ||||
|             # User and group sync can happen at once, they have no dependencies on each other | ||||
|             group( | ||||
|                 ldap_sync_paginator(source, UserLDAPSynchronizer) | ||||
|                 + ldap_sync_paginator(source, GroupLDAPSynchronizer), | ||||
|             ), | ||||
|             # Membership sync needs to run afterwards | ||||
|             group( | ||||
|                 ldap_sync_paginator(source, MembershipLDAPSynchronizer), | ||||
|             ), | ||||
|  | ||||
|         # The order of these operations needs to be preserved as each depends on the previous one(s) | ||||
|         # 1. User and group sync can happen simultaneously | ||||
|         # 2. Membership sync needs to run afterwards | ||||
|         # 3. Finally, user and group deletions can happen simultaneously | ||||
|         user_group_sync = ldap_sync_paginator(source, UserLDAPSynchronizer) + ldap_sync_paginator( | ||||
|             source, GroupLDAPSynchronizer | ||||
|         ) | ||||
|         task() | ||||
|         membership_sync = ldap_sync_paginator(source, MembershipLDAPSynchronizer) | ||||
|         user_group_deletion = ldap_sync_paginator( | ||||
|             source, UserLDAPForwardDeletion | ||||
|         ) + ldap_sync_paginator(source, GroupLDAPForwardDeletion) | ||||
|  | ||||
|         # Celery is buggy with empty groups, so we are careful only to add non-empty groups. | ||||
|         # See https://github.com/celery/celery/issues/9772 | ||||
|         task_groups = [] | ||||
|         if user_group_sync: | ||||
|             task_groups.append(group(user_group_sync)) | ||||
|         if membership_sync: | ||||
|             task_groups.append(group(membership_sync)) | ||||
|         if user_group_deletion: | ||||
|             task_groups.append(group(user_group_deletion)) | ||||
|  | ||||
|         all_tasks = chain(task_groups) | ||||
|         all_tasks() | ||||
|  | ||||
|  | ||||
| def ldap_sync_paginator(source: LDAPSource, sync: type[BaseLDAPSynchronizer]) -> list: | ||||
|  | ||||
| @ -2,6 +2,33 @@ | ||||
|  | ||||
| from ldap3 import MOCK_SYNC, OFFLINE_SLAPD_2_4, Connection, Server | ||||
|  | ||||
| # The mock modifies these in place, so we have to define them per string | ||||
| user_in_slapd_dn = "cn=user_in_slapd_cn,ou=users,dc=goauthentik,dc=io" | ||||
| user_in_slapd_cn = "user_in_slapd_cn" | ||||
| user_in_slapd_uid = "user_in_slapd_uid" | ||||
| user_in_slapd_object_class = "person" | ||||
| user_in_slapd = { | ||||
|     "dn": user_in_slapd_dn, | ||||
|     "attributes": { | ||||
|         "cn": user_in_slapd_cn, | ||||
|         "uid": user_in_slapd_uid, | ||||
|         "objectClass": user_in_slapd_object_class, | ||||
|     }, | ||||
| } | ||||
| group_in_slapd_dn = "cn=user_in_slapd_cn,ou=groups,dc=goauthentik,dc=io" | ||||
| group_in_slapd_cn = "group_in_slapd_cn" | ||||
| group_in_slapd_uid = "group_in_slapd_uid" | ||||
| group_in_slapd_object_class = "groupOfNames" | ||||
| group_in_slapd = { | ||||
|     "dn": group_in_slapd_dn, | ||||
|     "attributes": { | ||||
|         "cn": group_in_slapd_cn, | ||||
|         "uid": group_in_slapd_uid, | ||||
|         "objectClass": group_in_slapd_object_class, | ||||
|         "member": [user_in_slapd["dn"]], | ||||
|     }, | ||||
| } | ||||
|  | ||||
|  | ||||
| def mock_slapd_connection(password: str) -> Connection: | ||||
|     """Create mock SLAPD connection""" | ||||
| @ -96,5 +123,14 @@ def mock_slapd_connection(password: str) -> Connection: | ||||
|             "objectClass": "posixAccount", | ||||
|         }, | ||||
|     ) | ||||
|     # Known user and group | ||||
|     connection.strategy.add_entry( | ||||
|         user_in_slapd["dn"], | ||||
|         user_in_slapd["attributes"], | ||||
|     ) | ||||
|     connection.strategy.add_entry( | ||||
|         group_in_slapd["dn"], | ||||
|         group_in_slapd["attributes"], | ||||
|     ) | ||||
|     connection.bind() | ||||
|     return connection | ||||
|  | ||||
| @ -13,14 +13,26 @@ from authentik.events.system_tasks import TaskStatus | ||||
| from authentik.lib.generators import generate_id, generate_key | ||||
| from authentik.lib.sync.outgoing.exceptions import StopSync | ||||
| from authentik.lib.utils.reflection import class_to_path | ||||
| from authentik.sources.ldap.models import LDAPSource, LDAPSourcePropertyMapping | ||||
| from authentik.sources.ldap.models import ( | ||||
|     GroupLDAPSourceConnection, | ||||
|     LDAPSource, | ||||
|     LDAPSourcePropertyMapping, | ||||
|     UserLDAPSourceConnection, | ||||
| ) | ||||
| from authentik.sources.ldap.sync.forward_delete_users import DELETE_CHUNK_SIZE | ||||
| from authentik.sources.ldap.sync.groups import GroupLDAPSynchronizer | ||||
| from authentik.sources.ldap.sync.membership import MembershipLDAPSynchronizer | ||||
| from authentik.sources.ldap.sync.users import UserLDAPSynchronizer | ||||
| from authentik.sources.ldap.tasks import ldap_sync, ldap_sync_all | ||||
| from authentik.sources.ldap.tests.mock_ad import mock_ad_connection | ||||
| from authentik.sources.ldap.tests.mock_freeipa import mock_freeipa_connection | ||||
| from authentik.sources.ldap.tests.mock_slapd import mock_slapd_connection | ||||
| from authentik.sources.ldap.tests.mock_slapd import ( | ||||
|     group_in_slapd_cn, | ||||
|     group_in_slapd_uid, | ||||
|     mock_slapd_connection, | ||||
|     user_in_slapd_cn, | ||||
|     user_in_slapd_uid, | ||||
| ) | ||||
|  | ||||
| LDAP_PASSWORD = generate_key() | ||||
|  | ||||
| @ -257,11 +269,55 @@ class LDAPSyncTests(TestCase): | ||||
|         self.source.group_membership_field = "memberUid" | ||||
|         self.source.user_object_filter = "(objectClass=posixAccount)" | ||||
|         self.source.group_object_filter = "(objectClass=posixGroup)" | ||||
|         self.source.user_membership_attribute = "uid" | ||||
|         self.source.user_property_mappings.set( | ||||
|             LDAPSourcePropertyMapping.objects.filter( | ||||
|             [ | ||||
|                 *LDAPSourcePropertyMapping.objects.filter( | ||||
|                     Q(managed__startswith="goauthentik.io/sources/ldap/default") | ||||
|                     | Q(managed__startswith="goauthentik.io/sources/ldap/openldap") | ||||
|                 ).all(), | ||||
|                 LDAPSourcePropertyMapping.objects.create( | ||||
|                     name="name", | ||||
|                     expression='return {"attributes": {"uid": list_flatten(ldap.get("uid"))}}', | ||||
|                 ), | ||||
|             ] | ||||
|         ) | ||||
|         self.source.group_property_mappings.set( | ||||
|             LDAPSourcePropertyMapping.objects.filter( | ||||
|                 managed="goauthentik.io/sources/ldap/openldap-cn" | ||||
|             ) | ||||
|         ) | ||||
|         connection = MagicMock(return_value=mock_slapd_connection(LDAP_PASSWORD)) | ||||
|         with patch("authentik.sources.ldap.models.LDAPSource.connection", connection): | ||||
|             self.source.save() | ||||
|             user_sync = UserLDAPSynchronizer(self.source) | ||||
|             user_sync.sync_full() | ||||
|             group_sync = GroupLDAPSynchronizer(self.source) | ||||
|             group_sync.sync_full() | ||||
|             membership_sync = MembershipLDAPSynchronizer(self.source) | ||||
|             membership_sync.sync_full() | ||||
|             # Test if membership mapping based on memberUid works. | ||||
|             posix_group = Group.objects.filter(name="group-posix").first() | ||||
|             self.assertTrue(posix_group.users.filter(name="user-posix").exists()) | ||||
|  | ||||
|     def test_sync_groups_openldap_posix_group_nonstandard_membership_attribute(self): | ||||
|         """Test posix group sync""" | ||||
|         self.source.object_uniqueness_field = "cn" | ||||
|         self.source.group_membership_field = "memberUid" | ||||
|         self.source.user_object_filter = "(objectClass=posixAccount)" | ||||
|         self.source.group_object_filter = "(objectClass=posixGroup)" | ||||
|         self.source.user_membership_attribute = "cn" | ||||
|         self.source.user_property_mappings.set( | ||||
|             [ | ||||
|                 *LDAPSourcePropertyMapping.objects.filter( | ||||
|                     Q(managed__startswith="goauthentik.io/sources/ldap/default") | ||||
|                     | Q(managed__startswith="goauthentik.io/sources/ldap/openldap") | ||||
|                 ).all(), | ||||
|                 LDAPSourcePropertyMapping.objects.create( | ||||
|                     name="name", | ||||
|                     expression='return {"attributes": {"cn": list_flatten(ldap.get("cn"))}}', | ||||
|                 ), | ||||
|             ] | ||||
|         ) | ||||
|         self.source.group_property_mappings.set( | ||||
|             LDAPSourcePropertyMapping.objects.filter( | ||||
| @ -308,3 +364,160 @@ class LDAPSyncTests(TestCase): | ||||
|         connection = MagicMock(return_value=mock_slapd_connection(LDAP_PASSWORD)) | ||||
|         with patch("authentik.sources.ldap.models.LDAPSource.connection", connection): | ||||
|             ldap_sync_all.delay().get() | ||||
|  | ||||
|     def test_user_deletion(self): | ||||
|         """Test user deletion""" | ||||
|         user = User.objects.create_user(username="not-in-the-source") | ||||
|         UserLDAPSourceConnection.objects.create( | ||||
|             user=user, source=self.source, identifier="not-in-the-source" | ||||
|         ) | ||||
|         self.source.object_uniqueness_field = "uid" | ||||
|         self.source.group_object_filter = "(objectClass=groupOfNames)" | ||||
|         self.source.delete_not_found_objects = True | ||||
|         self.source.save() | ||||
|  | ||||
|         connection = MagicMock(return_value=mock_slapd_connection(LDAP_PASSWORD)) | ||||
|         with patch("authentik.sources.ldap.models.LDAPSource.connection", connection): | ||||
|             ldap_sync_all.delay().get() | ||||
|         self.assertFalse(User.objects.filter(username="not-in-the-source").exists()) | ||||
|  | ||||
|     def test_user_deletion_still_in_source(self): | ||||
|         """Test that user is not deleted if it's still in the source""" | ||||
|         username = user_in_slapd_cn | ||||
|         identifier = user_in_slapd_uid | ||||
|         user = User.objects.create_user(username=username) | ||||
|         UserLDAPSourceConnection.objects.create( | ||||
|             user=user, source=self.source, identifier=identifier | ||||
|         ) | ||||
|         self.source.object_uniqueness_field = "uid" | ||||
|         self.source.group_object_filter = "(objectClass=groupOfNames)" | ||||
|         self.source.delete_not_found_objects = True | ||||
|         self.source.save() | ||||
|  | ||||
|         connection = MagicMock(return_value=mock_slapd_connection(LDAP_PASSWORD)) | ||||
|         with patch("authentik.sources.ldap.models.LDAPSource.connection", connection): | ||||
|             ldap_sync_all.delay().get() | ||||
|         self.assertTrue(User.objects.filter(username=username).exists()) | ||||
|  | ||||
|     def test_user_deletion_no_sync(self): | ||||
|         """Test that user is not deleted if sync_users is False""" | ||||
|         user = User.objects.create_user(username="not-in-the-source") | ||||
|         UserLDAPSourceConnection.objects.create( | ||||
|             user=user, source=self.source, identifier="not-in-the-source" | ||||
|         ) | ||||
|         self.source.object_uniqueness_field = "uid" | ||||
|         self.source.group_object_filter = "(objectClass=groupOfNames)" | ||||
|         self.source.delete_not_found_objects = True | ||||
|         self.source.sync_users = False | ||||
|         self.source.save() | ||||
|  | ||||
|         connection = MagicMock(return_value=mock_slapd_connection(LDAP_PASSWORD)) | ||||
|         with patch("authentik.sources.ldap.models.LDAPSource.connection", connection): | ||||
|             ldap_sync_all.delay().get() | ||||
|         self.assertTrue(User.objects.filter(username="not-in-the-source").exists()) | ||||
|  | ||||
|     def test_user_deletion_no_delete(self): | ||||
|         """Test that user is not deleted if delete_not_found_objects is False""" | ||||
|         user = User.objects.create_user(username="not-in-the-source") | ||||
|         UserLDAPSourceConnection.objects.create( | ||||
|             user=user, source=self.source, identifier="not-in-the-source" | ||||
|         ) | ||||
|         self.source.object_uniqueness_field = "uid" | ||||
|         self.source.group_object_filter = "(objectClass=groupOfNames)" | ||||
|         self.source.save() | ||||
|  | ||||
|         connection = MagicMock(return_value=mock_slapd_connection(LDAP_PASSWORD)) | ||||
|         with patch("authentik.sources.ldap.models.LDAPSource.connection", connection): | ||||
|             ldap_sync_all.delay().get() | ||||
|         self.assertTrue(User.objects.filter(username="not-in-the-source").exists()) | ||||
|  | ||||
|     def test_group_deletion(self): | ||||
|         """Test group deletion""" | ||||
|         group = Group.objects.create(name="not-in-the-source") | ||||
|         GroupLDAPSourceConnection.objects.create( | ||||
|             group=group, source=self.source, identifier="not-in-the-source" | ||||
|         ) | ||||
|         self.source.object_uniqueness_field = "uid" | ||||
|         self.source.group_object_filter = "(objectClass=groupOfNames)" | ||||
|         self.source.delete_not_found_objects = True | ||||
|         self.source.save() | ||||
|  | ||||
|         connection = MagicMock(return_value=mock_slapd_connection(LDAP_PASSWORD)) | ||||
|         with patch("authentik.sources.ldap.models.LDAPSource.connection", connection): | ||||
|             ldap_sync_all.delay().get() | ||||
|         self.assertFalse(Group.objects.filter(name="not-in-the-source").exists()) | ||||
|  | ||||
|     def test_group_deletion_still_in_source(self): | ||||
|         """Test that group is not deleted if it's still in the source""" | ||||
|         groupname = group_in_slapd_cn | ||||
|         identifier = group_in_slapd_uid | ||||
|         group = Group.objects.create(name=groupname) | ||||
|         GroupLDAPSourceConnection.objects.create( | ||||
|             group=group, source=self.source, identifier=identifier | ||||
|         ) | ||||
|         self.source.object_uniqueness_field = "uid" | ||||
|         self.source.group_object_filter = "(objectClass=groupOfNames)" | ||||
|         self.source.delete_not_found_objects = True | ||||
|         self.source.save() | ||||
|  | ||||
|         connection = MagicMock(return_value=mock_slapd_connection(LDAP_PASSWORD)) | ||||
|         with patch("authentik.sources.ldap.models.LDAPSource.connection", connection): | ||||
|             ldap_sync_all.delay().get() | ||||
|         self.assertTrue(Group.objects.filter(name=groupname).exists()) | ||||
|  | ||||
|     def test_group_deletion_no_sync(self): | ||||
|         """Test that group is not deleted if sync_groups is False""" | ||||
|         group = Group.objects.create(name="not-in-the-source") | ||||
|         GroupLDAPSourceConnection.objects.create( | ||||
|             group=group, source=self.source, identifier="not-in-the-source" | ||||
|         ) | ||||
|         self.source.object_uniqueness_field = "uid" | ||||
|         self.source.group_object_filter = "(objectClass=groupOfNames)" | ||||
|         self.source.delete_not_found_objects = True | ||||
|         self.source.sync_groups = False | ||||
|         self.source.save() | ||||
|  | ||||
|         connection = MagicMock(return_value=mock_slapd_connection(LDAP_PASSWORD)) | ||||
|         with patch("authentik.sources.ldap.models.LDAPSource.connection", connection): | ||||
|             ldap_sync_all.delay().get() | ||||
|         self.assertTrue(Group.objects.filter(name="not-in-the-source").exists()) | ||||
|  | ||||
|     def test_group_deletion_no_delete(self): | ||||
|         """Test that group is not deleted if delete_not_found_objects is False""" | ||||
|         group = Group.objects.create(name="not-in-the-source") | ||||
|         GroupLDAPSourceConnection.objects.create( | ||||
|             group=group, source=self.source, identifier="not-in-the-source" | ||||
|         ) | ||||
|         self.source.object_uniqueness_field = "uid" | ||||
|         self.source.group_object_filter = "(objectClass=groupOfNames)" | ||||
|         self.source.save() | ||||
|  | ||||
|         connection = MagicMock(return_value=mock_slapd_connection(LDAP_PASSWORD)) | ||||
|         with patch("authentik.sources.ldap.models.LDAPSource.connection", connection): | ||||
|             ldap_sync_all.delay().get() | ||||
|         self.assertTrue(Group.objects.filter(name="not-in-the-source").exists()) | ||||
|  | ||||
|     def test_batch_deletion(self): | ||||
|         """Test batch deletion""" | ||||
|         BATCH_SIZE = DELETE_CHUNK_SIZE + 1 | ||||
|         for i in range(BATCH_SIZE): | ||||
|             user = User.objects.create_user(username=f"not-in-the-source-{i}") | ||||
|             group = Group.objects.create(name=f"not-in-the-source-{i}") | ||||
|             group.users.add(user) | ||||
|             UserLDAPSourceConnection.objects.create( | ||||
|                 user=user, source=self.source, identifier=f"not-in-the-source-{i}-user" | ||||
|             ) | ||||
|             GroupLDAPSourceConnection.objects.create( | ||||
|                 group=group, source=self.source, identifier=f"not-in-the-source-{i}-group" | ||||
|             ) | ||||
|         self.source.object_uniqueness_field = "uid" | ||||
|         self.source.group_object_filter = "(objectClass=groupOfNames)" | ||||
|         self.source.delete_not_found_objects = True | ||||
|         self.source.save() | ||||
|  | ||||
|         connection = MagicMock(return_value=mock_slapd_connection(LDAP_PASSWORD)) | ||||
|         with patch("authentik.sources.ldap.models.LDAPSource.connection", connection): | ||||
|             ldap_sync_all.delay().get() | ||||
|  | ||||
|         self.assertFalse(User.objects.filter(username__startswith="not-in-the-source").exists()) | ||||
|         self.assertFalse(Group.objects.filter(name__startswith="not-in-the-source").exists()) | ||||
|  | ||||
| @ -9,6 +9,7 @@ from django.http.response import HttpResponseBadRequest | ||||
| from django.shortcuts import get_object_or_404, redirect | ||||
| from django.utils.decorators import method_decorator | ||||
| from django.utils.http import urlencode | ||||
| from django.utils.translation import gettext as _ | ||||
| from django.views import View | ||||
| from django.views.decorators.csrf import csrf_exempt | ||||
| from structlog.stdlib import get_logger | ||||
| @ -128,7 +129,9 @@ class InitiateView(View): | ||||
|         # otherwise we default to POST_AUTO, with direct redirect | ||||
|         if source.binding_type == SAMLBindingTypes.POST: | ||||
|             injected_stages.append(in_memory_stage(ConsentStageView)) | ||||
|             plan_kwargs[PLAN_CONTEXT_CONSENT_HEADER] = f"Continue to {source.name}" | ||||
|             plan_kwargs[PLAN_CONTEXT_CONSENT_HEADER] = _( | ||||
|                 "Continue to {source_name}".format(source_name=source.name) | ||||
|             ) | ||||
|         injected_stages.append(in_memory_stage(AutosubmitStageView)) | ||||
|         return self.handle_login_flow( | ||||
|             source, | ||||
|  | ||||
| @ -168,13 +168,10 @@ class AuthenticatorValidateStageDuoTests(FlowTestCase): | ||||
|             user__pk=self.user.pk, | ||||
|         ).first() | ||||
|         self.assertIsNotNone(event) | ||||
|         context = dict(event.context) | ||||
|         # The auth_method field is being obfuscated as it's marked as sensitive in Django 5.2 | ||||
|         auth_method = context.pop("auth_method") | ||||
|         self.assertIn(auth_method, ["auth_mfa", "********************"]) | ||||
|         self.assertEqual( | ||||
|             context, | ||||
|             event.context, | ||||
|             { | ||||
|                 "auth_method": "auth_mfa", | ||||
|                 "auth_method_args": { | ||||
|                     "mfa_devices": [ | ||||
|                         { | ||||
|  | ||||
| @ -151,9 +151,7 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase): | ||||
|             webauthn_user_verification=UserVerification.PREFERRED, | ||||
|         ) | ||||
|         stage.webauthn_allowed_device_types.set( | ||||
|             WebAuthnDeviceType.objects.filter( | ||||
|                 description="Android Authenticator with SafetyNet Attestation" | ||||
|             ) | ||||
|             WebAuthnDeviceType.objects.filter(description="YubiKey 5 Series") | ||||
|         ) | ||||
|         session = self.client.session | ||||
|         plan = FlowPlan(flow_pk=flow.pk.hex) | ||||
| @ -339,9 +337,7 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase): | ||||
|             device_classes=[DeviceClasses.WEBAUTHN], | ||||
|         ) | ||||
|         stage.webauthn_allowed_device_types.set( | ||||
|             WebAuthnDeviceType.objects.filter( | ||||
|                 description="Android Authenticator with SafetyNet Attestation" | ||||
|             ) | ||||
|             WebAuthnDeviceType.objects.filter(description="YubiKey 5 Series") | ||||
|         ) | ||||
|         session = self.client.session | ||||
|         plan = FlowPlan(flow_pk=flow.pk.hex) | ||||
|  | ||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| @ -141,9 +141,7 @@ class TestAuthenticatorWebAuthnStage(FlowTestCase): | ||||
|         """Test registration with restricted devices (fail)""" | ||||
|         webauthn_mds_import.delay(force=True).get() | ||||
|         self.stage.device_type_restrictions.set( | ||||
|             WebAuthnDeviceType.objects.filter( | ||||
|                 description="Android Authenticator with SafetyNet Attestation" | ||||
|             ) | ||||
|             WebAuthnDeviceType.objects.filter(description="YubiKey 5 Series") | ||||
|         ) | ||||
|  | ||||
|         plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]) | ||||
|  | ||||
| @ -4,6 +4,8 @@ from uuid import uuid4 | ||||
|  | ||||
| from django.http import HttpRequest, HttpResponse | ||||
| from django.utils.timezone import now | ||||
| from django.utils.translation import gettext as _ | ||||
| from rest_framework.exceptions import ValidationError | ||||
| from rest_framework.fields import CharField | ||||
|  | ||||
| from authentik.core.api.utils import PassiveSerializer | ||||
| @ -47,6 +49,11 @@ class ConsentChallengeResponse(ChallengeResponse): | ||||
|     component = CharField(default="ak-stage-consent") | ||||
|     token = CharField(required=True) | ||||
|  | ||||
|     def validate_token(self, token: str): | ||||
|         if token != self.stage.executor.request.session[SESSION_KEY_CONSENT_TOKEN]: | ||||
|             raise ValidationError(_("Invalid consent token, re-showing prompt")) | ||||
|         return token | ||||
|  | ||||
|  | ||||
| class ConsentStageView(ChallengeStageView): | ||||
|     """Simple consent checker.""" | ||||
| @ -120,9 +127,6 @@ class ConsentStageView(ChallengeStageView): | ||||
|         return super().get(request, *args, **kwargs) | ||||
|  | ||||
|     def challenge_valid(self, response: ChallengeResponse) -> HttpResponse: | ||||
|         if response.data["token"] != self.request.session[SESSION_KEY_CONSENT_TOKEN]: | ||||
|             self.logger.info("Invalid consent token, re-showing prompt") | ||||
|             return self.get(self.request) | ||||
|         if self.should_always_prompt(): | ||||
|             return self.executor.stage_ok() | ||||
|         current_stage: ConsentStage = self.executor.current_stage | ||||
|  | ||||
| @ -17,6 +17,7 @@ from authentik.flows.views.executor import SESSION_KEY_PLAN | ||||
| from authentik.lib.generators import generate_id | ||||
| from authentik.stages.consent.models import ConsentMode, ConsentStage, UserConsent | ||||
| from authentik.stages.consent.stage import ( | ||||
|     PLAN_CONTEXT_CONSENT_HEADER, | ||||
|     PLAN_CONTEXT_CONSENT_PERMISSIONS, | ||||
|     SESSION_KEY_CONSENT_TOKEN, | ||||
| ) | ||||
| @ -33,6 +34,40 @@ class TestConsentStage(FlowTestCase): | ||||
|             slug=generate_id(), | ||||
|         ) | ||||
|  | ||||
|     def test_mismatched_token(self): | ||||
|         """Test incorrect token""" | ||||
|         flow = create_test_flow(FlowDesignation.AUTHENTICATION) | ||||
|         stage = ConsentStage.objects.create(name=generate_id(), mode=ConsentMode.ALWAYS_REQUIRE) | ||||
|         binding = FlowStageBinding.objects.create(target=flow, stage=stage, order=2) | ||||
|  | ||||
|         plan = FlowPlan(flow_pk=flow.pk.hex, bindings=[binding], markers=[StageMarker()]) | ||||
|         session = self.client.session | ||||
|         session[SESSION_KEY_PLAN] = plan | ||||
|         session.save() | ||||
|         response = self.client.get( | ||||
|             reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), | ||||
|         ) | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|  | ||||
|         session = self.client.session | ||||
|         response = self.client.post( | ||||
|             reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), | ||||
|             { | ||||
|                 "token": generate_id(), | ||||
|             }, | ||||
|         ) | ||||
|  | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|         self.assertStageResponse( | ||||
|             response, | ||||
|             flow, | ||||
|             component="ak-stage-consent", | ||||
|             response_errors={ | ||||
|                 "token": [{"string": "Invalid consent token, re-showing prompt", "code": "invalid"}] | ||||
|             }, | ||||
|         ) | ||||
|         self.assertFalse(UserConsent.objects.filter(user=self.user).exists()) | ||||
|  | ||||
|     def test_always_required(self): | ||||
|         """Test always required consent""" | ||||
|         flow = create_test_flow(FlowDesignation.AUTHENTICATION) | ||||
| @ -158,6 +193,7 @@ class TestConsentStage(FlowTestCase): | ||||
|             context={ | ||||
|                 PLAN_CONTEXT_APPLICATION: self.application, | ||||
|                 PLAN_CONTEXT_CONSENT_PERMISSIONS: [PermissionDict(id="foo", name="foo-desc")], | ||||
|                 PLAN_CONTEXT_CONSENT_HEADER: "test header", | ||||
|             }, | ||||
|         ) | ||||
|         session = self.client.session | ||||
|  | ||||
							
								
								
									
										38
									
								
								authentik/stages/email/flow.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								authentik/stages/email/flow.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,38 @@ | ||||
| from base64 import b64encode | ||||
| from copy import deepcopy | ||||
| from pickle import dumps  # nosec | ||||
|  | ||||
| from django.utils.translation import gettext as _ | ||||
|  | ||||
| from authentik.flows.models import FlowToken, in_memory_stage | ||||
| from authentik.flows.planner import PLAN_CONTEXT_IS_RESTORED, FlowPlan | ||||
| from authentik.stages.consent.stage import PLAN_CONTEXT_CONSENT_HEADER, ConsentStageView | ||||
|  | ||||
|  | ||||
| def pickle_flow_token_for_email(plan: FlowPlan): | ||||
|     """Insert a consent stage into the flow plan and pickle it for a FlowToken, | ||||
|     to be sent via Email. This is to prevent automated email scanners, which sometimes | ||||
|     open links in emails in a full browser from breaking the link.""" | ||||
|     plan_copy = deepcopy(plan) | ||||
|     plan_copy.insert_stage(in_memory_stage(EmailTokenRevocationConsentStageView), index=0) | ||||
|     plan_copy.context[PLAN_CONTEXT_CONSENT_HEADER] = _("Continue to confirm this email address.") | ||||
|     data = dumps(plan_copy) | ||||
|     return b64encode(data).decode() | ||||
|  | ||||
|  | ||||
| class EmailTokenRevocationConsentStageView(ConsentStageView): | ||||
|  | ||||
|     def get(self, request, *args, **kwargs): | ||||
|         token: FlowToken = self.executor.plan.context[PLAN_CONTEXT_IS_RESTORED] | ||||
|         try: | ||||
|             token.refresh_from_db() | ||||
|         except FlowToken.DoesNotExist: | ||||
|             return self.executor.stage_invalid( | ||||
|                 _("Link was already used, please request a new link.") | ||||
|             ) | ||||
|         return super().get(request, *args, **kwargs) | ||||
|  | ||||
|     def challenge_valid(self, response): | ||||
|         token: FlowToken = self.executor.plan.context[PLAN_CONTEXT_IS_RESTORED] | ||||
|         token.delete() | ||||
|         return super().challenge_valid(response) | ||||
| @ -23,6 +23,7 @@ from authentik.flows.stage import ChallengeStageView | ||||
| from authentik.flows.views.executor import QS_KEY_TOKEN, QS_QUERY | ||||
| from authentik.lib.utils.errors import exception_to_string | ||||
| from authentik.lib.utils.time import timedelta_from_string | ||||
| from authentik.stages.email.flow import pickle_flow_token_for_email | ||||
| from authentik.stages.email.models import EmailStage | ||||
| from authentik.stages.email.tasks import send_mails | ||||
| from authentik.stages.email.utils import TemplateEmailMessage | ||||
| @ -86,7 +87,8 @@ class EmailStageView(ChallengeStageView): | ||||
|                 user=pending_user, | ||||
|                 identifier=identifier, | ||||
|                 flow=self.executor.flow, | ||||
|                 _plan=FlowToken.pickle(self.executor.plan), | ||||
|                 _plan=pickle_flow_token_for_email(self.executor.plan), | ||||
|                 revoke_on_execution=False, | ||||
|             ) | ||||
|         token = tokens.first() | ||||
|         # Check if token is expired and rotate key if so | ||||
|  | ||||
| @ -100,8 +100,10 @@ def send_mail( | ||||
|         # Because we use the Message-ID as UID for the task, manually assign it | ||||
|         message_object.extra_headers["Message-ID"] = message_id | ||||
|  | ||||
|         # Add the logo (we can't add it in the previous message since MIMEImage | ||||
|         # can't be converted to json) | ||||
|         # Add the logo if it is used in the email body (we can't add it in the | ||||
|         # previous message since MIMEImage can't be converted to json) | ||||
|         body = get_email_body(message_object) | ||||
|         if "cid:logo" in body: | ||||
|             message_object.attach(logo_data()) | ||||
|  | ||||
|         if ( | ||||
|  | ||||
| @ -96,7 +96,7 @@ | ||||
|                 <table width="100%" style="background-color: #FFFFFF; border-spacing: 0; margin-top: 15px;"> | ||||
|                   <tr height="80"> | ||||
|                     <td align="center" style="padding: 20px 0;"> | ||||
|                       <img src="{% block logo_url %}cid:logo.png{% endblock %}" border="0=" alt="authentik logo" class="flexibleImage logo"> | ||||
|                       <img src="{% block logo_url %}cid:logo{% endblock %}" border="0=" alt="authentik logo" class="flexibleImage logo"> | ||||
|                     </td> | ||||
|                   </tr> | ||||
|                   {% block content %} | ||||
|  | ||||
| @ -174,5 +174,5 @@ class TestEmailStageSending(FlowTestCase): | ||||
|                 response = self.client.post(url) | ||||
|             response = self.client.post(url) | ||||
|             self.assertEqual(response.status_code, 200) | ||||
|             self.assertTrue(len(mail.outbox) >= 1) | ||||
|             self.assertGreaterEqual(len(mail.outbox), 1) | ||||
|             self.assertEqual(mail.outbox[0].subject, "authentik") | ||||
|  | ||||
| @ -17,6 +17,7 @@ from authentik.flows.tests import FlowTestCase | ||||
| from authentik.flows.views.executor import QS_KEY_TOKEN, SESSION_KEY_PLAN, FlowExecutorView | ||||
| from authentik.lib.config import CONFIG | ||||
| from authentik.lib.generators import generate_id | ||||
| from authentik.stages.consent.stage import SESSION_KEY_CONSENT_TOKEN | ||||
| from authentik.stages.email.models import EmailStage | ||||
| from authentik.stages.email.stage import PLAN_CONTEXT_EMAIL_OVERRIDE, EmailStageView | ||||
|  | ||||
| @ -160,6 +161,17 @@ class TestEmailStage(FlowTestCase): | ||||
|                     kwargs={"flow_slug": self.flow.slug}, | ||||
|                 ) | ||||
|             ) | ||||
|             self.assertStageResponse(response, self.flow, component="ak-stage-consent") | ||||
|             response = self.client.post( | ||||
|                 reverse( | ||||
|                     "authentik_api:flow-executor", | ||||
|                     kwargs={"flow_slug": self.flow.slug}, | ||||
|                 ), | ||||
|                 data={ | ||||
|                     "token": self.client.session[SESSION_KEY_CONSENT_TOKEN], | ||||
|                 }, | ||||
|                 follow=True, | ||||
|             ) | ||||
|  | ||||
|             self.assertEqual(response.status_code, 200) | ||||
|             self.assertStageRedirects(response, reverse("authentik_core:root-redirect")) | ||||
| @ -182,6 +194,7 @@ class TestEmailStage(FlowTestCase): | ||||
|         # Set flow token user to a different user | ||||
|         token: FlowToken = FlowToken.objects.get(user=self.user) | ||||
|         token.user = create_test_admin_user() | ||||
|         token.revoke_on_execution = True | ||||
|         token.save() | ||||
|  | ||||
|         with patch("authentik.flows.views.executor.FlowExecutorView.cancel", MagicMock()): | ||||
|  | ||||
| @ -19,7 +19,8 @@ def logo_data() -> MIMEImage: | ||||
|         path = Path("web/dist/assets/icons/icon_left_brand.png") | ||||
|     with open(path, "rb") as _logo_file: | ||||
|         logo = MIMEImage(_logo_file.read()) | ||||
|     logo.add_header("Content-ID", "logo.png") | ||||
|     logo.add_header("Content-ID", "<logo>") | ||||
|     logo.add_header("Content-Disposition", "inline", filename="logo.png") | ||||
|     return logo | ||||
|  | ||||
|  | ||||
|  | ||||
| @ -101,9 +101,9 @@ class BoundSessionMiddleware(SessionMiddleware): | ||||
|             SESSION_KEY_BINDING_GEO, GeoIPBinding.NO_BINDING | ||||
|         ) | ||||
|         if configured_binding_net != NetworkBinding.NO_BINDING: | ||||
|             self.recheck_session_net(configured_binding_net, last_ip, new_ip) | ||||
|             BoundSessionMiddleware.recheck_session_net(configured_binding_net, last_ip, new_ip) | ||||
|         if configured_binding_geo != GeoIPBinding.NO_BINDING: | ||||
|             self.recheck_session_geo(configured_binding_geo, last_ip, new_ip) | ||||
|             BoundSessionMiddleware.recheck_session_geo(configured_binding_geo, last_ip, new_ip) | ||||
|         # If we got to this point without any error being raised, we need to | ||||
|         # update the last saved IP to the current one | ||||
|         if SESSION_KEY_BINDING_NET in request.session or SESSION_KEY_BINDING_GEO in request.session: | ||||
| @ -111,7 +111,8 @@ class BoundSessionMiddleware(SessionMiddleware): | ||||
|             # (== basically requires the user to be logged in) | ||||
|             request.session[request.session.model.Keys.LAST_IP] = new_ip | ||||
|  | ||||
|     def recheck_session_net(self, binding: NetworkBinding, last_ip: str, new_ip: str): | ||||
|     @staticmethod | ||||
|     def recheck_session_net(binding: NetworkBinding, last_ip: str, new_ip: str): | ||||
|         """Check network/ASN binding""" | ||||
|         last_asn = ASN_CONTEXT_PROCESSOR.asn(last_ip) | ||||
|         new_asn = ASN_CONTEXT_PROCESSOR.asn(new_ip) | ||||
| @ -158,7 +159,8 @@ class BoundSessionMiddleware(SessionMiddleware): | ||||
|                     new_ip, | ||||
|                 ) | ||||
|  | ||||
|     def recheck_session_geo(self, binding: GeoIPBinding, last_ip: str, new_ip: str): | ||||
|     @staticmethod | ||||
|     def recheck_session_geo(binding: GeoIPBinding, last_ip: str, new_ip: str): | ||||
|         """Check GeoIP binding""" | ||||
|         last_geo = GEOIP_CONTEXT_PROCESSOR.city(last_ip) | ||||
|         new_geo = GEOIP_CONTEXT_PROCESSOR.city(new_ip) | ||||
| @ -179,8 +181,8 @@ class BoundSessionMiddleware(SessionMiddleware): | ||||
|             if last_geo.continent != new_geo.continent: | ||||
|                 raise SessionBindingBroken( | ||||
|                     "geoip.continent", | ||||
|                     last_geo.continent, | ||||
|                     new_geo.continent, | ||||
|                     last_geo.continent.to_dict(), | ||||
|                     new_geo.continent.to_dict(), | ||||
|                     last_ip, | ||||
|                     new_ip, | ||||
|                 ) | ||||
| @ -192,8 +194,8 @@ class BoundSessionMiddleware(SessionMiddleware): | ||||
|             if last_geo.country != new_geo.country: | ||||
|                 raise SessionBindingBroken( | ||||
|                     "geoip.country", | ||||
|                     last_geo.country, | ||||
|                     new_geo.country, | ||||
|                     last_geo.country.to_dict(), | ||||
|                     new_geo.country.to_dict(), | ||||
|                     last_ip, | ||||
|                     new_ip, | ||||
|                 ) | ||||
| @ -202,8 +204,8 @@ class BoundSessionMiddleware(SessionMiddleware): | ||||
|             if last_geo.city != new_geo.city: | ||||
|                 raise SessionBindingBroken( | ||||
|                     "geoip.city", | ||||
|                     last_geo.city, | ||||
|                     new_geo.city, | ||||
|                     last_geo.city.to_dict(), | ||||
|                     new_geo.city.to_dict(), | ||||
|                     last_ip, | ||||
|                     new_ip, | ||||
|                 ) | ||||
|  | ||||
| @ -11,7 +11,7 @@ from rest_framework.fields import BooleanField, CharField | ||||
| from authentik.core.models import Session, User | ||||
| from authentik.events.middleware import audit_ignore | ||||
| from authentik.flows.challenge import ChallengeResponse, WithUserInfoChallenge | ||||
| from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, PLAN_CONTEXT_SOURCE | ||||
| from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER | ||||
| from authentik.flows.stage import ChallengeStageView | ||||
| from authentik.lib.utils.time import timedelta_from_string | ||||
| from authentik.root.middleware import ClientIPMiddleware | ||||
| @ -108,10 +108,6 @@ class UserLoginStageView(ChallengeStageView): | ||||
|             flow_slug=self.executor.flow.slug, | ||||
|             session_duration=delta, | ||||
|         ) | ||||
|         # Only show success message if we don't have a source in the flow | ||||
|         # as sources show their own success messages | ||||
|         if not self.executor.plan.context.get(PLAN_CONTEXT_SOURCE, None): | ||||
|             messages.success(self.request, _("Successfully logged in!")) | ||||
|         if self.executor.current_stage.terminate_other_sessions: | ||||
|             Session.objects.filter( | ||||
|                 authenticatedsession__user=user, | ||||
|  | ||||
| @ -3,6 +3,7 @@ | ||||
| from time import sleep | ||||
| from unittest.mock import patch | ||||
|  | ||||
| from django.http import HttpRequest | ||||
| from django.urls import reverse | ||||
| from django.utils.timezone import now | ||||
|  | ||||
| @ -17,7 +18,12 @@ from authentik.flows.views.executor import SESSION_KEY_PLAN | ||||
| from authentik.lib.generators import generate_id | ||||
| from authentik.lib.utils.time import timedelta_from_string | ||||
| from authentik.root.middleware import ClientIPMiddleware | ||||
| from authentik.stages.user_login.models import UserLoginStage | ||||
| from authentik.stages.user_login.middleware import ( | ||||
|     BoundSessionMiddleware, | ||||
|     SessionBindingBroken, | ||||
|     logout_extra, | ||||
| ) | ||||
| from authentik.stages.user_login.models import GeoIPBinding, NetworkBinding, UserLoginStage | ||||
|  | ||||
|  | ||||
| class TestUserLoginStage(FlowTestCase): | ||||
| @ -192,3 +198,52 @@ class TestUserLoginStage(FlowTestCase): | ||||
|         self.assertStageRedirects(response, reverse("authentik_core:root-redirect")) | ||||
|         response = self.client.get(reverse("authentik_api:application-list")) | ||||
|         self.assertEqual(response.status_code, 403) | ||||
|  | ||||
|     def test_binding_net_break_log(self): | ||||
|         """Test logout_extra with exception""" | ||||
|         # IPs from https://github.com/maxmind/MaxMind-DB/blob/main/source-data/GeoLite2-ASN-Test.json | ||||
|         for args, expect in [ | ||||
|             [[NetworkBinding.BIND_ASN, "8.8.8.8", "8.8.8.8"], ["network.missing"]], | ||||
|             [[NetworkBinding.BIND_ASN, "1.0.0.1", "1.128.0.1"], ["network.asn"]], | ||||
|             [ | ||||
|                 [NetworkBinding.BIND_ASN_NETWORK, "12.81.96.1", "12.81.128.1"], | ||||
|                 ["network.asn_network"], | ||||
|             ], | ||||
|             [[NetworkBinding.BIND_ASN_NETWORK_IP, "1.0.0.1", "1.0.0.2"], ["network.ip"]], | ||||
|         ]: | ||||
|             with self.subTest(args[0]): | ||||
|                 with self.assertRaises(SessionBindingBroken) as cm: | ||||
|                     BoundSessionMiddleware.recheck_session_net(*args) | ||||
|                 self.assertEqual(cm.exception.reason, expect[0]) | ||||
|                 # Ensure the request can be logged without throwing errors | ||||
|                 self.client.force_login(self.user) | ||||
|                 request = HttpRequest() | ||||
|                 request.session = self.client.session | ||||
|                 request.user = self.user | ||||
|                 logout_extra(request, cm.exception) | ||||
|  | ||||
|     def test_binding_geo_break_log(self): | ||||
|         """Test logout_extra with exception""" | ||||
|         # IPs from https://github.com/maxmind/MaxMind-DB/blob/main/source-data/GeoLite2-City-Test.json | ||||
|         for args, expect in [ | ||||
|             [[GeoIPBinding.BIND_CONTINENT, "8.8.8.8", "8.8.8.8"], ["geoip.missing"]], | ||||
|             [[GeoIPBinding.BIND_CONTINENT, "2.125.160.216", "67.43.156.1"], ["geoip.continent"]], | ||||
|             [ | ||||
|                 [GeoIPBinding.BIND_CONTINENT_COUNTRY, "81.2.69.142", "89.160.20.112"], | ||||
|                 ["geoip.country"], | ||||
|             ], | ||||
|             [ | ||||
|                 [GeoIPBinding.BIND_CONTINENT_COUNTRY_CITY, "2.125.160.216", "81.2.69.142"], | ||||
|                 ["geoip.city"], | ||||
|             ], | ||||
|         ]: | ||||
|             with self.subTest(args[0]): | ||||
|                 with self.assertRaises(SessionBindingBroken) as cm: | ||||
|                     BoundSessionMiddleware.recheck_session_geo(*args) | ||||
|                 self.assertEqual(cm.exception.reason, expect[0]) | ||||
|                 # Ensure the request can be logged without throwing errors | ||||
|                 self.client.force_login(self.user) | ||||
|                 request = HttpRequest() | ||||
|                 request.session = self.client.session | ||||
|                 request.user = self.user | ||||
|                 logout_extra(request, cm.exception) | ||||
|  | ||||
| @ -2,7 +2,7 @@ | ||||
|     "$schema": "http://json-schema.org/draft-07/schema", | ||||
|     "$id": "https://goauthentik.io/blueprints/schema.json", | ||||
|     "type": "object", | ||||
|     "title": "authentik 2025.4.1 Blueprint schema", | ||||
|     "title": "authentik 2025.6.3 Blueprint schema", | ||||
|     "required": [ | ||||
|         "version", | ||||
|         "entries" | ||||
| @ -8147,6 +8147,12 @@ | ||||
|                     "title": "Group membership field", | ||||
|                     "description": "Field which contains members of a group." | ||||
|                 }, | ||||
|                 "user_membership_attribute": { | ||||
|                     "type": "string", | ||||
|                     "minLength": 1, | ||||
|                     "title": "User membership attribute", | ||||
|                     "description": "Attribute which matches the value of `group_membership_field`." | ||||
|                 }, | ||||
|                 "object_uniqueness_field": { | ||||
|                     "type": "string", | ||||
|                     "minLength": 1, | ||||
| @ -8180,6 +8186,11 @@ | ||||
|                     "type": "boolean", | ||||
|                     "title": "Lookup groups from user", | ||||
|                     "description": "Lookup group membership based on a user attribute instead of a group attribute. This allows nested group resolution on systems like FreeIPA and Active Directory" | ||||
|                 }, | ||||
|                 "delete_not_found_objects": { | ||||
|                     "type": "boolean", | ||||
|                     "title": "Delete not found objects", | ||||
|                     "description": "Delete authentik users and groups which were previously supplied by this source, but are now missing from it." | ||||
|                 } | ||||
|             }, | ||||
|             "required": [] | ||||
|  | ||||
| @ -31,7 +31,7 @@ services: | ||||
|     volumes: | ||||
|       - redis:/data | ||||
|   server: | ||||
|     image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.4.1} | ||||
|     image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.6.3} | ||||
|     restart: unless-stopped | ||||
|     command: server | ||||
|     environment: | ||||
| @ -55,7 +55,7 @@ services: | ||||
|       redis: | ||||
|         condition: service_healthy | ||||
|   worker: | ||||
|     image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.4.1} | ||||
|     image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.6.3} | ||||
|     restart: unless-stopped | ||||
|     command: worker | ||||
|     environment: | ||||
|  | ||||
							
								
								
									
										2
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								go.mod
									
									
									
									
									
								
							| @ -27,7 +27,7 @@ require ( | ||||
| 	github.com/spf13/cobra v1.9.1 | ||||
| 	github.com/stretchr/testify v1.10.0 | ||||
| 	github.com/wwt/guac v1.3.2 | ||||
| 	goauthentik.io/api/v3 v3.2025041.2 | ||||
| 	goauthentik.io/api/v3 v3.2025041.4 | ||||
| 	golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab | ||||
| 	golang.org/x/oauth2 v0.30.0 | ||||
| 	golang.org/x/sync v0.14.0 | ||||
|  | ||||
							
								
								
									
										4
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								go.sum
									
									
									
									
									
								
							| @ -290,8 +290,8 @@ go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y | ||||
| go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= | ||||
| go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= | ||||
| go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= | ||||
| goauthentik.io/api/v3 v3.2025041.2 h1:vFYYnhcDcxL95RczZwhzt3i4LptFXMvIRN+vgf8sQYg= | ||||
| goauthentik.io/api/v3 v3.2025041.2/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw= | ||||
| goauthentik.io/api/v3 v3.2025041.4 h1:cGqzWYnUHrWDoaXWDpIL/kWnX9sFrIhkYDye0P0OEAo= | ||||
| goauthentik.io/api/v3 v3.2025041.4/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw= | ||||
| golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= | ||||
| golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= | ||||
| golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= | ||||
|  | ||||
| @ -33,4 +33,4 @@ func UserAgent() string { | ||||
| 	return fmt.Sprintf("authentik@%s", FullVersion()) | ||||
| } | ||||
|  | ||||
| const VERSION = "2025.4.1" | ||||
| const VERSION = "2025.6.3" | ||||
|  | ||||
| @ -28,16 +28,18 @@ func NewSessionBinder(si server.LDAPServerInstance, oldBinder bind.Binder) *Sess | ||||
| 		si:  si, | ||||
| 		log: log.WithField("logger", "authentik.outpost.ldap.binder.session"), | ||||
| 	} | ||||
| 	if oldBinder != nil { | ||||
| 		if oldSb, ok := oldBinder.(*SessionBinder); ok { | ||||
| 			sb.DirectBinder = oldSb.DirectBinder | ||||
| 			sb.sessions = oldSb.sessions | ||||
| 			sb.log.Debug("re-initialised session binder") | ||||
| 	} else { | ||||
| 			return sb | ||||
| 		} | ||||
| 	} | ||||
| 	sb.sessions = ttlcache.New(ttlcache.WithDisableTouchOnHit[Credentials, ldap.LDAPResultCode]()) | ||||
| 	sb.DirectBinder = *direct.NewDirectBinder(si) | ||||
| 	go sb.sessions.Start() | ||||
| 	sb.log.Debug("initialised session binder") | ||||
| 	} | ||||
| 	return sb | ||||
| } | ||||
|  | ||||
|  | ||||
| @ -16,6 +16,7 @@ import ( | ||||
| 	memorybind "goauthentik.io/internal/outpost/ldap/bind/memory" | ||||
| 	"goauthentik.io/internal/outpost/ldap/constants" | ||||
| 	"goauthentik.io/internal/outpost/ldap/flags" | ||||
| 	"goauthentik.io/internal/outpost/ldap/search" | ||||
| 	directsearch "goauthentik.io/internal/outpost/ldap/search/direct" | ||||
| 	memorysearch "goauthentik.io/internal/outpost/ldap/search/memory" | ||||
| ) | ||||
| @ -85,7 +86,11 @@ func (ls *LDAPServer) Refresh() error { | ||||
| 			providers[idx].certUUID = *kp | ||||
| 		} | ||||
| 		if *provider.SearchMode.Ptr() == api.LDAPAPIACCESSMODE_CACHED { | ||||
| 			providers[idx].searcher = memorysearch.NewMemorySearcher(providers[idx]) | ||||
| 			var oldSearcher search.Searcher | ||||
| 			if existing != nil { | ||||
| 				oldSearcher = existing.searcher | ||||
| 			} | ||||
| 			providers[idx].searcher = memorysearch.NewMemorySearcher(providers[idx], oldSearcher) | ||||
| 		} else if *provider.SearchMode.Ptr() == api.LDAPAPIACCESSMODE_DIRECT { | ||||
| 			providers[idx].searcher = directsearch.NewDirectSearcher(providers[idx]) | ||||
| 		} | ||||
|  | ||||
| @ -31,13 +31,26 @@ type MemorySearcher struct { | ||||
| 	groups []api.Group | ||||
| } | ||||
|  | ||||
| func NewMemorySearcher(si server.LDAPServerInstance) *MemorySearcher { | ||||
| func NewMemorySearcher(si server.LDAPServerInstance, existing search.Searcher) *MemorySearcher { | ||||
| 	ms := &MemorySearcher{ | ||||
| 		si:  si, | ||||
| 		log: log.WithField("logger", "authentik.outpost.ldap.searcher.memory"), | ||||
| 		ds:  direct.NewDirectSearcher(si), | ||||
| 	} | ||||
| 	if existing != nil { | ||||
| 		if ems, ok := existing.(*MemorySearcher); ok { | ||||
| 			ems.si = si | ||||
| 			ems.fetch() | ||||
| 			ems.log.Debug("re-initialised memory searcher") | ||||
| 			return ems | ||||
| 		} | ||||
| 	} | ||||
| 	ms.fetch() | ||||
| 	ms.log.Debug("initialised memory searcher") | ||||
| 	return ms | ||||
| } | ||||
|  | ||||
| func (ms *MemorySearcher) fetch() { | ||||
| 	// Error is not handled here, we get an empty/truncated list and the error is logged | ||||
| 	users, _ := ak.Paginator(ms.si.GetAPIClient().CoreApi.CoreUsersList(context.TODO()).IncludeGroups(true), ak.PaginatorOptions{ | ||||
| 		PageSize: 100, | ||||
| @ -49,7 +62,6 @@ func NewMemorySearcher(si server.LDAPServerInstance) *MemorySearcher { | ||||
| 		Logger:   ms.log, | ||||
| 	}) | ||||
| 	ms.groups = groups | ||||
| 	return ms | ||||
| } | ||||
|  | ||||
| func (ms *MemorySearcher) SearchBase(req *search.Request) (ldap.ServerSearchResult, error) { | ||||
|  | ||||
| @ -5,6 +5,7 @@ import ( | ||||
| 	"crypto/sha256" | ||||
| 	"crypto/tls" | ||||
| 	"encoding/gob" | ||||
| 	"encoding/hex" | ||||
| 	"fmt" | ||||
| 	"html/template" | ||||
| 	"net/http" | ||||
| @ -118,8 +119,8 @@ func NewApplication(p api.ProxyOutpostConfig, c *http.Client, server Server, old | ||||
| 	mux := mux.NewRouter() | ||||
|  | ||||
| 	// Save cookie name, based on hashed client ID | ||||
| 	h := sha256.New() | ||||
| 	bs := string(h.Sum([]byte(*p.ClientId))) | ||||
| 	hs := sha256.Sum256([]byte(*p.ClientId)) | ||||
| 	bs := hex.EncodeToString(hs[:]) | ||||
| 	sessionName := fmt.Sprintf("authentik_proxy_%s", bs[:8]) | ||||
|  | ||||
| 	// When HOST_BROWSER is set, use that as Host header for token requests to make the issuer match | ||||
|  | ||||
| @ -3,6 +3,7 @@ package application | ||||
| type ProxyClaims struct { | ||||
| 	UserAttributes  map[string]interface{} `json:"user_attributes"` | ||||
| 	BackendOverride string                 `json:"backend_override"` | ||||
| 	HostHeader      string                 `json:"host_header"` | ||||
| 	IsSuperuser     bool                   `json:"is_superuser"` | ||||
| } | ||||
|  | ||||
|  | ||||
| @ -74,7 +74,8 @@ func (a *Application) proxyModifyRequest(ou *url.URL) func(req *http.Request) { | ||||
| 		r.URL.Scheme = ou.Scheme | ||||
| 		r.URL.Host = ou.Host | ||||
| 		claims := a.getClaimsFromSession(r) | ||||
| 		if claims != nil && claims.Proxy != nil && claims.Proxy.BackendOverride != "" { | ||||
| 		if claims != nil && claims.Proxy != nil { | ||||
| 			if claims.Proxy.BackendOverride != "" { | ||||
| 				u, err := url.Parse(claims.Proxy.BackendOverride) | ||||
| 				if err != nil { | ||||
| 					a.log.WithField("backend_override", claims.Proxy.BackendOverride).WithError(err).Warning("failed parse user backend override") | ||||
| @ -83,6 +84,10 @@ func (a *Application) proxyModifyRequest(ou *url.URL) func(req *http.Request) { | ||||
| 					r.URL.Host = u.Host | ||||
| 				} | ||||
| 			} | ||||
| 			if claims.Proxy.HostHeader != "" { | ||||
| 				r.Host = claims.Proxy.HostHeader | ||||
| 			} | ||||
| 		} | ||||
| 		a.log.WithField("upstream_url", r.URL.String()).Trace("final upstream url") | ||||
| 	} | ||||
| } | ||||
|  | ||||
| @ -2,6 +2,7 @@ package radius | ||||
|  | ||||
| import ( | ||||
| 	"crypto/sha512" | ||||
| 	"encoding/hex" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/getsentry/sentry-go" | ||||
| @ -68,7 +69,9 @@ func (rs *RadiusServer) ServeRADIUS(w radius.ResponseWriter, r *radius.Request) | ||||
| 		} | ||||
| 	} | ||||
| 	if pi == nil { | ||||
| 		nr.Log().WithField("hashed_secret", string(sha512.New().Sum(r.Secret))).Warning("No provider found") | ||||
| 		hs := sha512.Sum512([]byte(r.Secret)) | ||||
| 		bs := hex.EncodeToString(hs[:]) | ||||
| 		nr.Log().WithField("hashed_secret", bs).Warning("No provider found") | ||||
| 		_ = w.Write(r.Response(radius.CodeAccessReject)) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| @ -67,11 +67,15 @@ func (ws *WebServer) configureStatic() { | ||||
|  | ||||
| 	// Media files, if backend is file | ||||
| 	if config.Get().Storage.Media.Backend == "file" { | ||||
| 		fsMedia := http.StripPrefix("/media", http.FileServer(http.Dir(config.Get().Storage.Media.File.Path))) | ||||
| 		indexLessRouter.PathPrefix(config.Get().Web.Path).PathPrefix("/media/").HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||||
| 		fsMedia := http.FileServer(http.Dir(config.Get().Storage.Media.File.Path)) | ||||
| 		indexLessRouter.PathPrefix(config.Get().Web.Path).PathPrefix("/media/").Handler(pathStripper( | ||||
| 			http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||||
| 				w.Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'; sandbox") | ||||
| 				fsMedia.ServeHTTP(w, r) | ||||
| 		}) | ||||
| 			}), | ||||
| 			"media/", | ||||
| 			config.Get().Web.Path, | ||||
| 		)) | ||||
| 	} | ||||
|  | ||||
| 	staticRouter.PathPrefix(config.Get().Web.Path).PathPrefix("/if/help/").Handler(pathStripper( | ||||
|  | ||||
| @ -26,7 +26,7 @@ Parameters: | ||||
|     Description: authentik Docker image | ||||
|   AuthentikVersion: | ||||
|     Type: String | ||||
|     Default: 2025.4.1 | ||||
|     Default: 2025.6.3 | ||||
|     Description: authentik Docker image tag | ||||
|   AuthentikServerCPU: | ||||
|     Type: Number | ||||
|  | ||||
										
											Binary file not shown.
										
									
								
							| @ -32,15 +32,17 @@ | ||||
| # datenschmutz, 2025 | ||||
| # 97cce0ae0cad2a2cc552d3165d04643e_de3d740, 2025 | ||||
| # Dominic Wagner <mail@dominic-wagner.de>, 2025 | ||||
| # Till-Frederik Riechard, 2025 | ||||
| # Alexander Mnich, 2025 | ||||
| #  | ||||
| #, fuzzy | ||||
| msgid "" | ||||
| msgstr "" | ||||
| "Project-Id-Version: PACKAGE VERSION\n" | ||||
| "Report-Msgid-Bugs-To: \n" | ||||
| "POT-Creation-Date: 2025-04-23 09:00+0000\n" | ||||
| "POT-Creation-Date: 2025-05-28 11:25+0000\n" | ||||
| "PO-Revision-Date: 2022-09-26 16:47+0000\n" | ||||
| "Last-Translator: Dominic Wagner <mail@dominic-wagner.de>, 2025\n" | ||||
| "Last-Translator: Alexander Mnich, 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" | ||||
| @ -132,6 +134,10 @@ msgstr "" | ||||
| msgid "Web Certificate used by the authentik Core webserver." | ||||
| msgstr "Vom Authentik-Core-Webserver verwendetes Zertifikat." | ||||
|  | ||||
| #: authentik/brands/models.py | ||||
| msgid "Certificates used for client authentication." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/brands/models.py | ||||
| msgid "Brand" | ||||
| msgstr "Marke" | ||||
| @ -405,7 +411,7 @@ msgstr "Eigenschaften" | ||||
|  | ||||
| #: authentik/core/models.py | ||||
| msgid "session data" | ||||
| msgstr "" | ||||
| msgstr "Sitzungsdaten" | ||||
|  | ||||
| #: authentik/core/models.py | ||||
| msgid "Session" | ||||
| @ -533,7 +539,7 @@ msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/policies/unique_password/models.py | ||||
| msgid "Number of passwords to check against." | ||||
| msgstr "" | ||||
| msgstr "Anzahl Passwörter, gegen die geprüft wird." | ||||
|  | ||||
| #: authentik/enterprise/policies/unique_password/models.py | ||||
| #: authentik/policies/password/models.py | ||||
| @ -543,18 +549,20 @@ msgstr "Passwort nicht im Kontext festgelegt" | ||||
| #: authentik/enterprise/policies/unique_password/models.py | ||||
| msgid "This password has been used previously. Please choose a different one." | ||||
| msgstr "" | ||||
| "Dieses Passwort wurde in Vergangenheit bereits verwendet. Bitte nutzen Sie " | ||||
| "ein anderes." | ||||
|  | ||||
| #: authentik/enterprise/policies/unique_password/models.py | ||||
| msgid "Password Uniqueness Policy" | ||||
| msgstr "" | ||||
| msgstr "Passwort-Einzigartigkeits-Richtlinie" | ||||
|  | ||||
| #: authentik/enterprise/policies/unique_password/models.py | ||||
| msgid "Password Uniqueness Policies" | ||||
| msgstr "" | ||||
| msgstr "Passwort-Einzigartigkeits-Richtlinien" | ||||
|  | ||||
| #: authentik/enterprise/policies/unique_password/models.py | ||||
| msgid "User Password History" | ||||
| msgstr "" | ||||
| msgstr "Nutzer-Passwort-Historie" | ||||
|  | ||||
| #: authentik/enterprise/policy.py | ||||
| msgid "Enterprise required to access this feature." | ||||
| @ -693,6 +701,33 @@ msgstr "Endgeräte" | ||||
| msgid "Verifying your browser..." | ||||
| msgstr "Verifiziere deinen Browser..." | ||||
|  | ||||
| #: authentik/enterprise/stages/mtls/models.py | ||||
| msgid "" | ||||
| "Configure certificate authorities to validate the certificate against. This " | ||||
| "option has a higher priority than the `client_certificate` option on " | ||||
| "`Brand`." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/stages/mtls/models.py | ||||
| msgid "Mutual TLS Stage" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/stages/mtls/models.py | ||||
| msgid "Mutual TLS Stages" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/stages/mtls/models.py | ||||
| msgid "Permissions to pass Certificates for outposts." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/stages/mtls/stage.py | ||||
| msgid "Certificate required but no certificate was given." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/stages/mtls/stage.py | ||||
| msgid "No user found for certificate." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/stages/source/models.py | ||||
| msgid "" | ||||
| "Amount of time a user can take to return from the source to continue the " | ||||
| @ -988,7 +1023,7 @@ msgstr "" | ||||
|  | ||||
| #: authentik/flows/models.py | ||||
| msgid "Evaluate policies when the Stage is presented to the user." | ||||
| msgstr "" | ||||
| msgstr "Richtlinien auswerten, wenn die Phase dem Benutzer angezeigt wird." | ||||
|  | ||||
| #: authentik/flows/models.py | ||||
| msgid "" | ||||
| @ -1043,9 +1078,12 @@ msgid "Starting full provider sync" | ||||
| msgstr "Starte komplette Provider Synchronisation." | ||||
|  | ||||
| #: authentik/lib/sync/outgoing/tasks.py | ||||
| #, python-brace-format | ||||
| msgid "Syncing page {page} of users" | ||||
| msgstr "Synchonisiere Benutzer Seite {page}" | ||||
| msgid "Syncing users" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/lib/sync/outgoing/tasks.py | ||||
| msgid "Syncing groups" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/lib/sync/outgoing/tasks.py | ||||
| #, python-brace-format | ||||
| @ -1593,11 +1631,11 @@ msgstr "ES256 (Asymmetrische Verschlüsselung)" | ||||
|  | ||||
| #: authentik/providers/oauth2/models.py | ||||
| msgid "ES384 (Asymmetric Encryption)" | ||||
| msgstr "" | ||||
| msgstr "ES384 (Asymmetrische Verschlüsselung)" | ||||
|  | ||||
| #: authentik/providers/oauth2/models.py | ||||
| msgid "ES512 (Asymmetric Encryption)" | ||||
| msgstr "" | ||||
| msgstr "ES5122 (Asymmetrische Verschlüsselung)" | ||||
|  | ||||
| #: authentik/providers/oauth2/models.py | ||||
| msgid "Scope used by the client" | ||||
| @ -2183,11 +2221,11 @@ msgstr "Standard" | ||||
|  | ||||
| #: authentik/providers/scim/models.py | ||||
| msgid "AWS" | ||||
| msgstr "" | ||||
| msgstr "AWS" | ||||
|  | ||||
| #: authentik/providers/scim/models.py | ||||
| msgid "Slack" | ||||
| msgstr "" | ||||
| msgstr "Slack" | ||||
|  | ||||
| #: authentik/providers/scim/models.py | ||||
| msgid "Base URL to SCIM requests, usually ends in /v2" | ||||
| @ -2199,7 +2237,7 @@ msgstr "Authentifizierungstoken" | ||||
|  | ||||
| #: authentik/providers/scim/models.py | ||||
| msgid "SCIM Compatibility Mode" | ||||
| msgstr "" | ||||
| msgstr "SCIM Kompatibilitätsmodus" | ||||
|  | ||||
| #: authentik/providers/scim/models.py | ||||
| msgid "Alter authentik behavior for vendor-specific SCIM implementations." | ||||
| @ -2231,7 +2269,7 @@ msgstr "Rollen" | ||||
|  | ||||
| #: authentik/rbac/models.py | ||||
| msgid "Initial Permissions" | ||||
| msgstr "" | ||||
| msgstr "Initiale Berechtigungen" | ||||
|  | ||||
| #: authentik/rbac/models.py | ||||
| msgid "System permission" | ||||
| @ -2487,6 +2525,12 @@ msgid "" | ||||
| "Active Directory" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/sources/ldap/models.py | ||||
| msgid "" | ||||
| "Delete authentik users and groups which were previously supplied by this " | ||||
| "source, but are now missing from it." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/sources/ldap/models.py | ||||
| msgid "LDAP Source" | ||||
| msgstr "LDAP Quelle" | ||||
| @ -2504,20 +2548,25 @@ msgid "LDAP Source Property Mappings" | ||||
| msgstr "LDAP Quelle Eigenschafts-Zuordnungen" | ||||
|  | ||||
| #: authentik/sources/ldap/models.py | ||||
| msgid "User LDAP Source Connection" | ||||
| msgid "" | ||||
| "Unique ID used while checking if this object still exists in the directory." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/sources/ldap/models.py | ||||
| msgid "User LDAP Source Connection" | ||||
| msgstr "Benutzer LDAP-Quellverbindung" | ||||
|  | ||||
| #: authentik/sources/ldap/models.py | ||||
| msgid "User LDAP Source Connections" | ||||
| msgstr "" | ||||
| msgstr "Benutzer LDAP-Quellverbindungen" | ||||
|  | ||||
| #: authentik/sources/ldap/models.py | ||||
| msgid "Group LDAP Source Connection" | ||||
| msgstr "" | ||||
| msgstr "LDAP Gruppen Quellverbindung" | ||||
|  | ||||
| #: authentik/sources/ldap/models.py | ||||
| msgid "Group LDAP Source Connections" | ||||
| msgstr "" | ||||
| msgstr "LDAP Gruppen Quellverbindungen" | ||||
|  | ||||
| #: authentik/sources/ldap/signals.py | ||||
| msgid "Password does not match Active Directory Complexity." | ||||
| @ -2530,7 +2579,7 @@ msgstr "Kein Token empfangen." | ||||
|  | ||||
| #: authentik/sources/oauth/models.py | ||||
| msgid "HTTP Basic Authentication" | ||||
| msgstr "" | ||||
| msgstr "HTTP Basic Authentifizierung" | ||||
|  | ||||
| #: authentik/sources/oauth/models.py | ||||
| msgid "Include the client ID and secret as request parameters" | ||||
| @ -2896,6 +2945,11 @@ msgstr "SAML Gruppen Quellverbindung" | ||||
| msgid "Group SAML Source Connections" | ||||
| msgstr "SAML Gruppen Quellverbindungen" | ||||
|  | ||||
| #: authentik/sources/saml/views.py | ||||
| #, python-brace-format | ||||
| msgid "Continue to {source_name}" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/sources/scim/models.py | ||||
| msgid "SCIM Source" | ||||
| msgstr "SCIM Quelle" | ||||
| @ -2930,7 +2984,7 @@ msgstr "Duo Geräte" | ||||
|  | ||||
| #: authentik/stages/authenticator_email/models.py | ||||
| msgid "Email OTP" | ||||
| msgstr "" | ||||
| msgstr "E-Mail Einmalpasswort" | ||||
|  | ||||
| #: authentik/stages/authenticator_email/models.py | ||||
| #: authentik/stages/email/models.py | ||||
| @ -2963,11 +3017,11 @@ msgstr "Beim Rendern der E-Mail-Vorlage ist ein Fehler aufgetreten" | ||||
|  | ||||
| #: authentik/stages/authenticator_email/models.py | ||||
| msgid "Email Device" | ||||
| msgstr "" | ||||
| msgstr "E-Mail Gerät" | ||||
|  | ||||
| #: authentik/stages/authenticator_email/models.py | ||||
| msgid "Email Devices" | ||||
| msgstr "" | ||||
| msgstr "E-Mail Geräte" | ||||
|  | ||||
| #: authentik/stages/authenticator_email/stage.py | ||||
| #: authentik/stages/authenticator_sms/stage.py | ||||
| @ -2977,7 +3031,7 @@ msgstr "Code stimmt nicht überein" | ||||
|  | ||||
| #: authentik/stages/authenticator_email/stage.py | ||||
| msgid "Invalid email" | ||||
| msgstr "" | ||||
| msgstr "Ungültige E-Mail" | ||||
|  | ||||
| #: authentik/stages/authenticator_email/templates/email/email_otp.html | ||||
| #: authentik/stages/email/templates/email/password_reset.html | ||||
| @ -3273,6 +3327,10 @@ msgstr "Zustimmung der Benutzer" | ||||
| msgid "User Consents" | ||||
| msgstr "Zustimmungen der Benutzer" | ||||
|  | ||||
| #: authentik/stages/consent/stage.py | ||||
| msgid "Invalid consent token, re-showing prompt" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/stages/deny/models.py | ||||
| msgid "Deny Stage" | ||||
| msgstr "Verweigerungsstufe" | ||||
| @ -3289,6 +3347,14 @@ msgstr "Dummy Stufe" | ||||
| msgid "Dummy Stages" | ||||
| msgstr "Dummy Stufen" | ||||
|  | ||||
| #: authentik/stages/email/flow.py | ||||
| msgid "Continue to confirm this email address." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/stages/email/flow.py | ||||
| msgid "Link was already used, please request a new link." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/stages/email/models.py | ||||
| msgid "Password Reset" | ||||
| msgstr "Passwort zurücksetzen" | ||||
| @ -3890,10 +3956,11 @@ msgstr "" | ||||
| #: authentik/tenants/models.py | ||||
| msgid "Reputation cannot decrease lower than this value. Zero or negative." | ||||
| msgstr "" | ||||
| "Reputation kann nicht niedriger als dieser Wert sein. Null oder negativ." | ||||
|  | ||||
| #: authentik/tenants/models.py | ||||
| msgid "Reputation cannot increase higher than this value. Zero or positive." | ||||
| msgstr "" | ||||
| msgstr "Reputation kann nicht höher als dieser Wert sein. Null oder positiv." | ||||
|  | ||||
| #: authentik/tenants/models.py | ||||
| msgid "The option configures the footer links on the flow executor pages." | ||||
|  | ||||
| @ -8,7 +8,7 @@ msgid "" | ||||
| msgstr "" | ||||
| "Project-Id-Version: PACKAGE VERSION\n" | ||||
| "Report-Msgid-Bugs-To: \n" | ||||
| "POT-Creation-Date: 2025-05-20 00:10+0000\n" | ||||
| "POT-Creation-Date: 2025-06-02 00:12+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" | ||||
| @ -961,8 +961,11 @@ msgid "Starting full provider sync" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/lib/sync/outgoing/tasks.py | ||||
| #, python-brace-format | ||||
| msgid "Syncing page {page} of users" | ||||
| msgid "Syncing users" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/lib/sync/outgoing/tasks.py | ||||
| msgid "Syncing groups" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/lib/sync/outgoing/tasks.py | ||||
| @ -2223,6 +2226,10 @@ msgstr "" | ||||
| msgid "Consider Objects matching this filter to be Users." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/sources/ldap/models.py | ||||
| msgid "Attribute which matches the value of `group_membership_field`." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/sources/ldap/models.py | ||||
| msgid "Field which contains members of a group." | ||||
| msgstr "" | ||||
| @ -2252,6 +2259,12 @@ msgid "" | ||||
| "Active Directory" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/sources/ldap/models.py | ||||
| msgid "" | ||||
| "Delete authentik users and groups which were previously supplied by this " | ||||
| "source, but are now missing from it." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/sources/ldap/models.py | ||||
| msgid "LDAP Source" | ||||
| msgstr "" | ||||
| @ -2268,6 +2281,11 @@ msgstr "" | ||||
| msgid "LDAP Source Property Mappings" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/sources/ldap/models.py | ||||
| msgid "" | ||||
| "Unique ID used while checking if this object still exists in the directory." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/sources/ldap/models.py | ||||
| msgid "User LDAP Source Connection" | ||||
| msgstr "" | ||||
| @ -2639,6 +2657,11 @@ msgstr "" | ||||
| msgid "Group SAML Source Connections" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/sources/saml/views.py | ||||
| #, python-brace-format | ||||
| msgid "Continue to {source_name}" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/sources/scim/models.py | ||||
| msgid "SCIM Source" | ||||
| msgstr "" | ||||
| @ -2994,6 +3017,10 @@ msgstr "" | ||||
| msgid "User Consents" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/stages/consent/stage.py | ||||
| msgid "Invalid consent token, re-showing prompt" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/stages/deny/models.py | ||||
| msgid "Deny Stage" | ||||
| msgstr "" | ||||
| @ -3010,6 +3037,14 @@ msgstr "" | ||||
| msgid "Dummy Stages" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/stages/email/flow.py | ||||
| msgid "Continue to confirm this email address." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/stages/email/flow.py | ||||
| msgid "Link was already used, please request a new link." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/stages/email/models.py | ||||
| msgid "Password Reset" | ||||
| msgstr "" | ||||
| @ -3462,10 +3497,6 @@ msgstr "" | ||||
| msgid "No Pending user to login." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/stages/user_login/stage.py | ||||
| msgid "Successfully logged in!" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/stages/user_logout/models.py | ||||
| msgid "User Logout Stage" | ||||
| msgstr "" | ||||
|  | ||||
										
											Binary file not shown.
										
									
								
							| @ -15,7 +15,7 @@ msgid "" | ||||
| msgstr "" | ||||
| "Project-Id-Version: PACKAGE VERSION\n" | ||||
| "Report-Msgid-Bugs-To: \n" | ||||
| "POT-Creation-Date: 2025-04-23 09:00+0000\n" | ||||
| "POT-Creation-Date: 2025-05-28 11:25+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" | ||||
| @ -109,6 +109,10 @@ msgstr "" | ||||
| msgid "Web Certificate used by the authentik Core webserver." | ||||
| msgstr "Certificado Web usado por el servidor web Core de authentik" | ||||
|  | ||||
| #: authentik/brands/models.py | ||||
| msgid "Certificates used for client authentication." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/brands/models.py | ||||
| msgid "Brand" | ||||
| msgstr "Marca" | ||||
| @ -671,6 +675,33 @@ msgstr "Dispositivos de Punto de Conexión" | ||||
| msgid "Verifying your browser..." | ||||
| msgstr "Verificando tu navegador..." | ||||
|  | ||||
| #: authentik/enterprise/stages/mtls/models.py | ||||
| msgid "" | ||||
| "Configure certificate authorities to validate the certificate against. This " | ||||
| "option has a higher priority than the `client_certificate` option on " | ||||
| "`Brand`." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/stages/mtls/models.py | ||||
| msgid "Mutual TLS Stage" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/stages/mtls/models.py | ||||
| msgid "Mutual TLS Stages" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/stages/mtls/models.py | ||||
| msgid "Permissions to pass Certificates for outposts." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/stages/mtls/stage.py | ||||
| msgid "Certificate required but no certificate was given." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/stages/mtls/stage.py | ||||
| msgid "No user found for certificate." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/stages/source/models.py | ||||
| msgid "" | ||||
| "Amount of time a user can take to return from the source to continue the " | ||||
| @ -1009,9 +1040,12 @@ msgid "Starting full provider sync" | ||||
| msgstr "Iniciando sincronización completa de proveedor" | ||||
|  | ||||
| #: authentik/lib/sync/outgoing/tasks.py | ||||
| #, python-brace-format | ||||
| msgid "Syncing page {page} of users" | ||||
| msgstr "Sincronizando página {page} de usuarios" | ||||
| msgid "Syncing users" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/lib/sync/outgoing/tasks.py | ||||
| msgid "Syncing groups" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/lib/sync/outgoing/tasks.py | ||||
| #, python-brace-format | ||||
| @ -2452,6 +2486,12 @@ msgid "" | ||||
| "Active Directory" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/sources/ldap/models.py | ||||
| msgid "" | ||||
| "Delete authentik users and groups which were previously supplied by this " | ||||
| "source, but are now missing from it." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/sources/ldap/models.py | ||||
| msgid "LDAP Source" | ||||
| msgstr "Fuente de LDAP" | ||||
| @ -2468,6 +2508,11 @@ msgstr "Asignación de Propiedades de Fuente de LDAP" | ||||
| msgid "LDAP Source Property Mappings" | ||||
| msgstr "Asignaciones de Propiedades de Fuente de LDAP" | ||||
|  | ||||
| #: authentik/sources/ldap/models.py | ||||
| msgid "" | ||||
| "Unique ID used while checking if this object still exists in the directory." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/sources/ldap/models.py | ||||
| msgid "User LDAP Source Connection" | ||||
| msgstr "" | ||||
| @ -2859,6 +2904,11 @@ msgstr "Conexión de Fuente de SAML de Grupo" | ||||
| msgid "Group SAML Source Connections" | ||||
| msgstr "Conexiones de Fuente de SAML de Grupo" | ||||
|  | ||||
| #: authentik/sources/saml/views.py | ||||
| #, python-brace-format | ||||
| msgid "Continue to {source_name}" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/sources/scim/models.py | ||||
| msgid "SCIM Source" | ||||
| msgstr "Fuente de SCIM" | ||||
| @ -3245,6 +3295,10 @@ msgstr "Consentimiento del usuario" | ||||
| msgid "User Consents" | ||||
| msgstr "Consentimientos del usuario" | ||||
|  | ||||
| #: authentik/stages/consent/stage.py | ||||
| msgid "Invalid consent token, re-showing prompt" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/stages/deny/models.py | ||||
| msgid "Deny Stage" | ||||
| msgstr "Etapa de denegación" | ||||
| @ -3261,6 +3315,14 @@ msgstr "Escenario ficticio" | ||||
| msgid "Dummy Stages" | ||||
| msgstr "Etapas ficticias" | ||||
|  | ||||
| #: authentik/stages/email/flow.py | ||||
| msgid "Continue to confirm this email address." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/stages/email/flow.py | ||||
| msgid "Link was already used, please request a new link." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/stages/email/models.py | ||||
| msgid "Password Reset" | ||||
| msgstr "Restablecimiento de contraseña" | ||||
|  | ||||
										
											Binary file not shown.
										
									
								
							| @ -15,7 +15,7 @@ msgid "" | ||||
| msgstr "" | ||||
| "Project-Id-Version: PACKAGE VERSION\n" | ||||
| "Report-Msgid-Bugs-To: \n" | ||||
| "POT-Creation-Date: 2025-04-23 09:00+0000\n" | ||||
| "POT-Creation-Date: 2025-05-28 11:25+0000\n" | ||||
| "PO-Revision-Date: 2022-09-26 16:47+0000\n" | ||||
| "Last-Translator: Ville Ranki, 2025\n" | ||||
| "Language-Team: Finnish (https://app.transifex.com/authentik/teams/119923/fi/)\n" | ||||
| @ -106,6 +106,10 @@ msgstr "" | ||||
| msgid "Web Certificate used by the authentik Core webserver." | ||||
| msgstr "Web-sertifikaatti, jota authentik Core -verkkopalvelin käyttää." | ||||
|  | ||||
| #: authentik/brands/models.py | ||||
| msgid "Certificates used for client authentication." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/brands/models.py | ||||
| msgid "Brand" | ||||
| msgstr "Brändi" | ||||
| @ -658,6 +662,33 @@ msgstr "Päätelaitteet" | ||||
| msgid "Verifying your browser..." | ||||
| msgstr "Selaintasi varmennetaan..." | ||||
|  | ||||
| #: authentik/enterprise/stages/mtls/models.py | ||||
| msgid "" | ||||
| "Configure certificate authorities to validate the certificate against. This " | ||||
| "option has a higher priority than the `client_certificate` option on " | ||||
| "`Brand`." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/stages/mtls/models.py | ||||
| msgid "Mutual TLS Stage" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/stages/mtls/models.py | ||||
| msgid "Mutual TLS Stages" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/stages/mtls/models.py | ||||
| msgid "Permissions to pass Certificates for outposts." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/stages/mtls/stage.py | ||||
| msgid "Certificate required but no certificate was given." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/stages/mtls/stage.py | ||||
| msgid "No user found for certificate." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/stages/source/models.py | ||||
| msgid "" | ||||
| "Amount of time a user can take to return from the source to continue the " | ||||
| @ -996,9 +1027,12 @@ msgid "Starting full provider sync" | ||||
| msgstr "Käynnistetään palveluntarjoajan täysi synkronisointi" | ||||
|  | ||||
| #: authentik/lib/sync/outgoing/tasks.py | ||||
| #, python-brace-format | ||||
| msgid "Syncing page {page} of users" | ||||
| msgstr "Synkronoidaan käyttäjien sivua {page}" | ||||
| msgid "Syncing users" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/lib/sync/outgoing/tasks.py | ||||
| msgid "Syncing groups" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/lib/sync/outgoing/tasks.py | ||||
| #, python-brace-format | ||||
| @ -2429,6 +2463,12 @@ msgid "" | ||||
| "Active Directory" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/sources/ldap/models.py | ||||
| msgid "" | ||||
| "Delete authentik users and groups which were previously supplied by this " | ||||
| "source, but are now missing from it." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/sources/ldap/models.py | ||||
| msgid "LDAP Source" | ||||
| msgstr "LDAP-lähde" | ||||
| @ -2445,6 +2485,11 @@ msgstr "LDAP-lähteen ominaisuuskytkentä" | ||||
| msgid "LDAP Source Property Mappings" | ||||
| msgstr "LDAP-lähteen ominaisuuskytkennät" | ||||
|  | ||||
| #: authentik/sources/ldap/models.py | ||||
| msgid "" | ||||
| "Unique ID used while checking if this object still exists in the directory." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/sources/ldap/models.py | ||||
| msgid "User LDAP Source Connection" | ||||
| msgstr "" | ||||
| @ -2837,6 +2882,11 @@ msgstr "Ryhmän SAML-lähteen yhteys" | ||||
| msgid "Group SAML Source Connections" | ||||
| msgstr "Ryhmän SAML-lähteen yhteydet" | ||||
|  | ||||
| #: authentik/sources/saml/views.py | ||||
| #, python-brace-format | ||||
| msgid "Continue to {source_name}" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/sources/scim/models.py | ||||
| msgid "SCIM Source" | ||||
| msgstr "SCIM-lähde" | ||||
| @ -3216,6 +3266,10 @@ msgstr "Käyttäjän hyväksyntä" | ||||
| msgid "User Consents" | ||||
| msgstr "Käyttäjän hyväksynnät" | ||||
|  | ||||
| #: authentik/stages/consent/stage.py | ||||
| msgid "Invalid consent token, re-showing prompt" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/stages/deny/models.py | ||||
| msgid "Deny Stage" | ||||
| msgstr "Kieltovaihe" | ||||
| @ -3232,6 +3286,14 @@ msgstr "Valevaihe" | ||||
| msgid "Dummy Stages" | ||||
| msgstr "Valevaiheet" | ||||
|  | ||||
| #: authentik/stages/email/flow.py | ||||
| msgid "Continue to confirm this email address." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/stages/email/flow.py | ||||
| msgid "Link was already used, please request a new link." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/stages/email/models.py | ||||
| msgid "Password Reset" | ||||
| msgstr "Salasanan nollaus" | ||||
|  | ||||
| @ -19,7 +19,7 @@ msgid "" | ||||
| msgstr "" | ||||
| "Project-Id-Version: PACKAGE VERSION\n" | ||||
| "Report-Msgid-Bugs-To: \n" | ||||
| "POT-Creation-Date: 2025-05-20 00:10+0000\n" | ||||
| "POT-Creation-Date: 2025-05-28 11:25+0000\n" | ||||
| "PO-Revision-Date: 2022-09-26 16:47+0000\n" | ||||
| "Last-Translator: Marc Schmitt, 2025\n" | ||||
| "Language-Team: French (https://app.transifex.com/authentik/teams/119923/fr/)\n" | ||||
| @ -1056,9 +1056,12 @@ msgid "Starting full provider sync" | ||||
| msgstr "Démarrage d'une synchronisation complète du fournisseur" | ||||
|  | ||||
| #: authentik/lib/sync/outgoing/tasks.py | ||||
| #, python-brace-format | ||||
| msgid "Syncing page {page} of users" | ||||
| msgstr "Synchronisation de la page {page} d'utilisateurs" | ||||
| msgid "Syncing users" | ||||
| msgstr "Synchronisation des utilisateurs" | ||||
|  | ||||
| #: authentik/lib/sync/outgoing/tasks.py | ||||
| msgid "Syncing groups" | ||||
| msgstr "Synchronisation des groupes" | ||||
|  | ||||
| #: authentik/lib/sync/outgoing/tasks.py | ||||
| #, python-brace-format | ||||
| @ -2508,6 +2511,14 @@ msgstr "" | ||||
| "plutôt que sur un attribut de groupe. Cela permet la résolution des groupes " | ||||
| "imbriqués sur des systèmes tels que FreeIPA et Active Directory." | ||||
|  | ||||
| #: authentik/sources/ldap/models.py | ||||
| msgid "" | ||||
| "Delete authentik users and groups which were previously supplied by this " | ||||
| "source, but are now missing from it." | ||||
| msgstr "" | ||||
| "Supprimer les utilisateurs et les groupes authentik qui étaient auparavant " | ||||
| "fournis par cette source, mais qui en sont maintenant absents." | ||||
|  | ||||
| #: authentik/sources/ldap/models.py | ||||
| msgid "LDAP Source" | ||||
| msgstr "Source LDAP" | ||||
| @ -2524,6 +2535,13 @@ msgstr "Mappage de propriété source LDAP" | ||||
| msgid "LDAP Source Property Mappings" | ||||
| msgstr "Mappages de propriété source LDAP" | ||||
|  | ||||
| #: authentik/sources/ldap/models.py | ||||
| msgid "" | ||||
| "Unique ID used while checking if this object still exists in the directory." | ||||
| msgstr "" | ||||
| "ID unique utilisé pour vérifier si cet objet existe toujours dans le " | ||||
| "répertoire." | ||||
|  | ||||
| #: authentik/sources/ldap/models.py | ||||
| msgid "User LDAP Source Connection" | ||||
| msgstr "Connexion de l'utilisateur à la source LDAP" | ||||
| @ -2918,6 +2936,11 @@ msgstr "Connexion du groupe à la source SAML" | ||||
| msgid "Group SAML Source Connections" | ||||
| msgstr "Connexions du groupe à la source SAML" | ||||
|  | ||||
| #: authentik/sources/saml/views.py | ||||
| #, python-brace-format | ||||
| msgid "Continue to {source_name}" | ||||
| msgstr "Continuer vers {source_name}" | ||||
|  | ||||
| #: authentik/sources/scim/models.py | ||||
| msgid "SCIM Source" | ||||
| msgstr "Source SCIM" | ||||
| @ -3308,6 +3331,10 @@ msgstr "Consentement Utilisateur" | ||||
| msgid "User Consents" | ||||
| msgstr "Consentements Utilisateur" | ||||
|  | ||||
| #: authentik/stages/consent/stage.py | ||||
| msgid "Invalid consent token, re-showing prompt" | ||||
| msgstr "Jeton de consentement invalide, réaffichage de l'invite" | ||||
|  | ||||
| #: authentik/stages/deny/models.py | ||||
| msgid "Deny Stage" | ||||
| msgstr "Étape de Refus" | ||||
| @ -3324,6 +3351,14 @@ msgstr "Étape factice" | ||||
| msgid "Dummy Stages" | ||||
| msgstr "Étapes factices" | ||||
|  | ||||
| #: authentik/stages/email/flow.py | ||||
| msgid "Continue to confirm this email address." | ||||
| msgstr "Continuer pour confirmer cette adresse courriel." | ||||
|  | ||||
| #: authentik/stages/email/flow.py | ||||
| msgid "Link was already used, please request a new link." | ||||
| msgstr "Ce lien a déjà été utilisé, veuillez en demander un nouveau." | ||||
|  | ||||
| #: authentik/stages/email/models.py | ||||
| msgid "Password Reset" | ||||
| msgstr "Réinitialiser le Mot de Passe" | ||||
|  | ||||
| @ -20,7 +20,7 @@ msgid "" | ||||
| msgstr "" | ||||
| "Project-Id-Version: PACKAGE VERSION\n" | ||||
| "Report-Msgid-Bugs-To: \n" | ||||
| "POT-Creation-Date: 2025-04-23 09:00+0000\n" | ||||
| "POT-Creation-Date: 2025-05-28 11:25+0000\n" | ||||
| "PO-Revision-Date: 2022-09-26 16:47+0000\n" | ||||
| "Last-Translator: Kowalski Dragon (kowalski7cc) <kowalski.7cc@gmail.com>, 2025\n" | ||||
| "Language-Team: Italian (https://app.transifex.com/authentik/teams/119923/it/)\n" | ||||
| @ -114,6 +114,10 @@ msgstr "" | ||||
| msgid "Web Certificate used by the authentik Core webserver." | ||||
| msgstr "Certificato Web utilizzato dal server Web authentik Core." | ||||
|  | ||||
| #: authentik/brands/models.py | ||||
| msgid "Certificates used for client authentication." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/brands/models.py | ||||
| msgid "Brand" | ||||
| msgstr "Brand" | ||||
| @ -672,6 +676,33 @@ msgstr "Dispositivi di Accesso" | ||||
| msgid "Verifying your browser..." | ||||
| msgstr "Verifica del tuo browser..." | ||||
|  | ||||
| #: authentik/enterprise/stages/mtls/models.py | ||||
| msgid "" | ||||
| "Configure certificate authorities to validate the certificate against. This " | ||||
| "option has a higher priority than the `client_certificate` option on " | ||||
| "`Brand`." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/stages/mtls/models.py | ||||
| msgid "Mutual TLS Stage" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/stages/mtls/models.py | ||||
| msgid "Mutual TLS Stages" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/stages/mtls/models.py | ||||
| msgid "Permissions to pass Certificates for outposts." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/stages/mtls/stage.py | ||||
| msgid "Certificate required but no certificate was given." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/stages/mtls/stage.py | ||||
| msgid "No user found for certificate." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/stages/source/models.py | ||||
| msgid "" | ||||
| "Amount of time a user can take to return from the source to continue the " | ||||
| @ -1018,9 +1049,12 @@ msgid "Starting full provider sync" | ||||
| msgstr "Avvio della sincronizzazione completa del provider" | ||||
|  | ||||
| #: authentik/lib/sync/outgoing/tasks.py | ||||
| #, python-brace-format | ||||
| msgid "Syncing page {page} of users" | ||||
| msgstr "Sincronizzando pagina {page} degli utenti" | ||||
| msgid "Syncing users" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/lib/sync/outgoing/tasks.py | ||||
| msgid "Syncing groups" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/lib/sync/outgoing/tasks.py | ||||
| #, python-brace-format | ||||
| @ -2463,6 +2497,12 @@ msgstr "" | ||||
| "attributo di gruppo. Questo consente la risoluzione di gruppi nidificati su " | ||||
| "sistemi come FreeIPA e Active Directory." | ||||
|  | ||||
| #: authentik/sources/ldap/models.py | ||||
| msgid "" | ||||
| "Delete authentik users and groups which were previously supplied by this " | ||||
| "source, but are now missing from it." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/sources/ldap/models.py | ||||
| msgid "LDAP Source" | ||||
| msgstr "Sorgente LDAP" | ||||
| @ -2479,6 +2519,11 @@ msgstr "Mappatura delle proprietà sorgente LDAP" | ||||
| msgid "LDAP Source Property Mappings" | ||||
| msgstr "Mappature delle proprietà della sorgente LDAP" | ||||
|  | ||||
| #: authentik/sources/ldap/models.py | ||||
| msgid "" | ||||
| "Unique ID used while checking if this object still exists in the directory." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/sources/ldap/models.py | ||||
| msgid "User LDAP Source Connection" | ||||
| msgstr "Connessione Sorgente LDAP Utente" | ||||
| @ -2872,6 +2917,11 @@ msgstr "Connessione sorgente SAML di gruppo" | ||||
| msgid "Group SAML Source Connections" | ||||
| msgstr "Connessioni sorgente SAML di gruppo" | ||||
|  | ||||
| #: authentik/sources/saml/views.py | ||||
| #, python-brace-format | ||||
| msgid "Continue to {source_name}" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/sources/scim/models.py | ||||
| msgid "SCIM Source" | ||||
| msgstr "Sorgente SCIM" | ||||
| @ -3269,6 +3319,10 @@ msgstr "Consenso utente" | ||||
| msgid "User Consents" | ||||
| msgstr "Consensi utente" | ||||
|  | ||||
| #: authentik/stages/consent/stage.py | ||||
| msgid "Invalid consent token, re-showing prompt" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/stages/deny/models.py | ||||
| msgid "Deny Stage" | ||||
| msgstr "Fase di negazione" | ||||
| @ -3285,6 +3339,14 @@ msgstr "Fase fittizia" | ||||
| msgid "Dummy Stages" | ||||
| msgstr "Fasi fittizie" | ||||
|  | ||||
| #: authentik/stages/email/flow.py | ||||
| msgid "Continue to confirm this email address." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/stages/email/flow.py | ||||
| msgid "Link was already used, please request a new link." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/stages/email/models.py | ||||
| msgid "Password Reset" | ||||
| msgstr "Ripristino password" | ||||
|  | ||||
| @ -12,7 +12,7 @@ msgid "" | ||||
| msgstr "" | ||||
| "Project-Id-Version: PACKAGE VERSION\n" | ||||
| "Report-Msgid-Bugs-To: \n" | ||||
| "POT-Creation-Date: 2025-04-23 09:00+0000\n" | ||||
| "POT-Creation-Date: 2025-05-28 11:25+0000\n" | ||||
| "PO-Revision-Date: 2022-09-26 16:47+0000\n" | ||||
| "Last-Translator: NavyStack, 2023\n" | ||||
| "Language-Team: Korean (https://app.transifex.com/authentik/teams/119923/ko/)\n" | ||||
| @ -99,6 +99,10 @@ msgstr "" | ||||
| msgid "Web Certificate used by the authentik Core webserver." | ||||
| msgstr "Authentik Core 웹서버에서 사용하는 웹 인증서." | ||||
|  | ||||
| #: authentik/brands/models.py | ||||
| msgid "Certificates used for client authentication." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/brands/models.py | ||||
| msgid "Brand" | ||||
| msgstr "" | ||||
| @ -625,6 +629,33 @@ msgstr "" | ||||
| msgid "Verifying your browser..." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/stages/mtls/models.py | ||||
| msgid "" | ||||
| "Configure certificate authorities to validate the certificate against. This " | ||||
| "option has a higher priority than the `client_certificate` option on " | ||||
| "`Brand`." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/stages/mtls/models.py | ||||
| msgid "Mutual TLS Stage" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/stages/mtls/models.py | ||||
| msgid "Mutual TLS Stages" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/stages/mtls/models.py | ||||
| msgid "Permissions to pass Certificates for outposts." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/stages/mtls/stage.py | ||||
| msgid "Certificate required but no certificate was given." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/stages/mtls/stage.py | ||||
| msgid "No user found for certificate." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/stages/source/models.py | ||||
| msgid "" | ||||
| "Amount of time a user can take to return from the source to continue the " | ||||
| @ -946,8 +977,11 @@ msgid "Starting full provider sync" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/lib/sync/outgoing/tasks.py | ||||
| #, python-brace-format | ||||
| msgid "Syncing page {page} of users" | ||||
| msgid "Syncing users" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/lib/sync/outgoing/tasks.py | ||||
| msgid "Syncing groups" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/lib/sync/outgoing/tasks.py | ||||
| @ -2263,6 +2297,12 @@ msgid "" | ||||
| "Active Directory" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/sources/ldap/models.py | ||||
| msgid "" | ||||
| "Delete authentik users and groups which were previously supplied by this " | ||||
| "source, but are now missing from it." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/sources/ldap/models.py | ||||
| msgid "LDAP Source" | ||||
| msgstr "LDAP 소스" | ||||
| @ -2279,6 +2319,11 @@ msgstr "" | ||||
| msgid "LDAP Source Property Mappings" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/sources/ldap/models.py | ||||
| msgid "" | ||||
| "Unique ID used while checking if this object still exists in the directory." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/sources/ldap/models.py | ||||
| msgid "User LDAP Source Connection" | ||||
| msgstr "" | ||||
| @ -2657,6 +2702,11 @@ msgstr "" | ||||
| msgid "Group SAML Source Connections" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/sources/saml/views.py | ||||
| #, python-brace-format | ||||
| msgid "Continue to {source_name}" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/sources/scim/models.py | ||||
| msgid "SCIM Source" | ||||
| msgstr "" | ||||
| @ -3017,6 +3067,10 @@ msgstr "사용자 동의" | ||||
| msgid "User Consents" | ||||
| msgstr "사용자 동의" | ||||
|  | ||||
| #: authentik/stages/consent/stage.py | ||||
| msgid "Invalid consent token, re-showing prompt" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/stages/deny/models.py | ||||
| msgid "Deny Stage" | ||||
| msgstr "거부 스테이지" | ||||
| @ -3033,6 +3087,14 @@ msgstr "더미 스테이지" | ||||
| msgid "Dummy Stages" | ||||
| msgstr "더미 스테이지" | ||||
|  | ||||
| #: authentik/stages/email/flow.py | ||||
| msgid "Continue to confirm this email address." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/stages/email/flow.py | ||||
| msgid "Link was already used, please request a new link." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/stages/email/models.py | ||||
| msgid "Password Reset" | ||||
| msgstr "비밀번호 초기화" | ||||
|  | ||||
										
											Binary file not shown.
										
									
								
							| @ -19,7 +19,7 @@ msgid "" | ||||
| msgstr "" | ||||
| "Project-Id-Version: PACKAGE VERSION\n" | ||||
| "Report-Msgid-Bugs-To: \n" | ||||
| "POT-Creation-Date: 2025-04-11 00:10+0000\n" | ||||
| "POT-Creation-Date: 2025-05-28 11:25+0000\n" | ||||
| "PO-Revision-Date: 2022-09-26 16:47+0000\n" | ||||
| "Last-Translator: Dany Sluijk, 2025\n" | ||||
| "Language-Team: Dutch (https://app.transifex.com/authentik/teams/119923/nl/)\n" | ||||
| @ -113,6 +113,10 @@ msgstr "" | ||||
| msgid "Web Certificate used by the authentik Core webserver." | ||||
| msgstr "Webcertificaat gebruikt door de authentik Core-webserver." | ||||
|  | ||||
| #: authentik/brands/models.py | ||||
| msgid "Certificates used for client authentication." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/brands/models.py | ||||
| msgid "Brand" | ||||
| msgstr "Merk" | ||||
| @ -191,6 +195,7 @@ msgid "User's display name." | ||||
| msgstr "Weergavenaam van de gebruiker." | ||||
|  | ||||
| #: authentik/core/models.py authentik/providers/oauth2/models.py | ||||
| #: authentik/rbac/models.py | ||||
| msgid "User" | ||||
| msgstr "Gebruiker" | ||||
|  | ||||
| @ -379,6 +384,18 @@ msgstr "Eigenschapskoppeling" | ||||
| msgid "Property Mappings" | ||||
| msgstr "Eigenschapskoppelingen" | ||||
|  | ||||
| #: authentik/core/models.py | ||||
| msgid "session data" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/core/models.py | ||||
| msgid "Session" | ||||
| msgstr "Sessie" | ||||
|  | ||||
| #: authentik/core/models.py | ||||
| msgid "Sessions" | ||||
| msgstr "Sessies" | ||||
|  | ||||
| #: authentik/core/models.py | ||||
| msgid "Authenticated Session" | ||||
| msgstr "Geauthenticeerde Sessie" | ||||
| @ -486,6 +503,38 @@ msgstr "Licentie Gebruik" | ||||
| msgid "License Usage Records" | ||||
| msgstr "Licentie Gebruik Records" | ||||
|  | ||||
| #: authentik/enterprise/policies/unique_password/models.py | ||||
| #: authentik/policies/password/models.py | ||||
| msgid "Field key to check, field keys defined in Prompt stages are available." | ||||
| msgstr "" | ||||
| "Veldsleutel om te controleren, veldsleutels gedefinieerd in Prompt-stadia " | ||||
| "zijn beschikbaar." | ||||
|  | ||||
| #: authentik/enterprise/policies/unique_password/models.py | ||||
| msgid "Number of passwords to check against." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/policies/unique_password/models.py | ||||
| #: authentik/policies/password/models.py | ||||
| msgid "Password not set in context" | ||||
| msgstr "Wachtwoord niet ingesteld in context" | ||||
|  | ||||
| #: authentik/enterprise/policies/unique_password/models.py | ||||
| msgid "This password has been used previously. Please choose a different one." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/policies/unique_password/models.py | ||||
| msgid "Password Uniqueness Policy" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/policies/unique_password/models.py | ||||
| msgid "Password Uniqueness Policies" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/policies/unique_password/models.py | ||||
| msgid "User Password History" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/policy.py | ||||
| msgid "Enterprise required to access this feature." | ||||
| msgstr "Enterprise benodigd voor toegang tot deze functie." | ||||
| @ -622,6 +671,33 @@ msgstr "" | ||||
| msgid "Verifying your browser..." | ||||
| msgstr "Uw browser wordt geverifieerd..." | ||||
|  | ||||
| #: authentik/enterprise/stages/mtls/models.py | ||||
| msgid "" | ||||
| "Configure certificate authorities to validate the certificate against. This " | ||||
| "option has a higher priority than the `client_certificate` option on " | ||||
| "`Brand`." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/stages/mtls/models.py | ||||
| msgid "Mutual TLS Stage" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/stages/mtls/models.py | ||||
| msgid "Mutual TLS Stages" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/stages/mtls/models.py | ||||
| msgid "Permissions to pass Certificates for outposts." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/stages/mtls/stage.py | ||||
| msgid "Certificate required but no certificate was given." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/stages/mtls/stage.py | ||||
| msgid "No user found for certificate." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/stages/source/models.py | ||||
| msgid "" | ||||
| "Amount of time a user can take to return from the source to continue the " | ||||
| @ -963,8 +1039,11 @@ msgid "Starting full provider sync" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/lib/sync/outgoing/tasks.py | ||||
| #, python-brace-format | ||||
| msgid "Syncing page {page} of users" | ||||
| msgid "Syncing users" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/lib/sync/outgoing/tasks.py | ||||
| msgid "Syncing groups" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/lib/sync/outgoing/tasks.py | ||||
| @ -1265,12 +1344,6 @@ msgstr "" | ||||
| msgid "Clear Policy's cache metrics" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/policies/password/models.py | ||||
| msgid "Field key to check, field keys defined in Prompt stages are available." | ||||
| msgstr "" | ||||
| "Veldsleutel om te controleren, veldsleutels gedefinieerd in Prompt-stadia " | ||||
| "zijn beschikbaar." | ||||
|  | ||||
| #: authentik/policies/password/models.py | ||||
| msgid "How many times the password hash is allowed to be on haveibeenpwned" | ||||
| msgstr "Hoe vaak het wachtwoordhash op haveibeenpwned mag voorkomen" | ||||
| @ -1282,10 +1355,6 @@ msgstr "" | ||||
| "Als de zxcvbn-score gelijk is aan of lager is dan deze waarde, zal het " | ||||
| "beleid falen." | ||||
|  | ||||
| #: authentik/policies/password/models.py | ||||
| msgid "Password not set in context" | ||||
| msgstr "Wachtwoord niet ingesteld in context" | ||||
|  | ||||
| #: authentik/policies/password/models.py | ||||
| msgid "Invalid password." | ||||
| msgstr "" | ||||
| @ -1327,20 +1396,6 @@ msgstr "Reputatie Score" | ||||
| msgid "Reputation Scores" | ||||
| msgstr "Reputatie Scores" | ||||
|  | ||||
| #: authentik/policies/templates/policies/buffer.html | ||||
| msgid "Waiting for authentication..." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/policies/templates/policies/buffer.html | ||||
| msgid "" | ||||
| "You're already authenticating in another tab. This page will refresh once " | ||||
| "authentication is completed." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/policies/templates/policies/buffer.html | ||||
| msgid "Authenticate in this tab" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/policies/templates/policies/denied.html | ||||
| msgid "Permission denied" | ||||
| msgstr "Toestemming geweigerd" | ||||
| @ -2160,6 +2215,10 @@ msgstr "" | ||||
| msgid "Roles" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/rbac/models.py | ||||
| msgid "Initial Permissions" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/rbac/models.py | ||||
| msgid "System permission" | ||||
| msgstr "" | ||||
| @ -2392,6 +2451,12 @@ msgid "" | ||||
| "Active Directory" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/sources/ldap/models.py | ||||
| msgid "" | ||||
| "Delete authentik users and groups which were previously supplied by this " | ||||
| "source, but are now missing from it." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/sources/ldap/models.py | ||||
| msgid "LDAP Source" | ||||
| msgstr "LDAP-bron" | ||||
| @ -2408,6 +2473,27 @@ msgstr "" | ||||
| msgid "LDAP Source Property Mappings" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/sources/ldap/models.py | ||||
| msgid "" | ||||
| "Unique ID used while checking if this object still exists in the directory." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/sources/ldap/models.py | ||||
| msgid "User LDAP Source Connection" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/sources/ldap/models.py | ||||
| msgid "User LDAP Source Connections" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/sources/ldap/models.py | ||||
| msgid "Group LDAP Source Connection" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/sources/ldap/models.py | ||||
| msgid "Group LDAP Source Connections" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/sources/ldap/signals.py | ||||
| msgid "Password does not match Active Directory Complexity." | ||||
| msgstr "" | ||||
| @ -2417,6 +2503,14 @@ msgstr "" | ||||
| msgid "No token received." | ||||
| msgstr "Geen token ontvangen." | ||||
|  | ||||
| #: authentik/sources/oauth/models.py | ||||
| msgid "HTTP Basic Authentication" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/sources/oauth/models.py | ||||
| msgid "Include the client ID and secret as request parameters" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/sources/oauth/models.py | ||||
| msgid "Request Token URL" | ||||
| msgstr "URL voor aanvragen van token" | ||||
| @ -2458,6 +2552,12 @@ msgstr "" | ||||
| msgid "Additional Scopes" | ||||
| msgstr "Aanvullende scopes" | ||||
|  | ||||
| #: authentik/sources/oauth/models.py | ||||
| msgid "" | ||||
| "How to perform authentication during an authorization_code token request " | ||||
| "flow" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/sources/oauth/models.py | ||||
| msgid "OAuth Source" | ||||
| msgstr "OAuth-bron" | ||||
| @ -2769,6 +2869,11 @@ msgstr "" | ||||
| msgid "Group SAML Source Connections" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/sources/saml/views.py | ||||
| #, python-brace-format | ||||
| msgid "Continue to {source_name}" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/sources/scim/models.py | ||||
| msgid "SCIM Source" | ||||
| msgstr "" | ||||
| @ -3142,6 +3247,10 @@ msgstr "Gebruikerstoestemming" | ||||
| msgid "User Consents" | ||||
| msgstr "Gebruikersinstemmingen" | ||||
|  | ||||
| #: authentik/stages/consent/stage.py | ||||
| msgid "Invalid consent token, re-showing prompt" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/stages/deny/models.py | ||||
| msgid "Deny Stage" | ||||
| msgstr "Weigerfase" | ||||
| @ -3158,6 +3267,14 @@ msgstr "Dummystadium" | ||||
| msgid "Dummy Stages" | ||||
| msgstr "Dummystadia" | ||||
|  | ||||
| #: authentik/stages/email/flow.py | ||||
| msgid "Continue to confirm this email address." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/stages/email/flow.py | ||||
| msgid "Link was already used, please request a new link." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/stages/email/models.py | ||||
| msgid "Password Reset" | ||||
| msgstr "Wachtwoordherstel" | ||||
| @ -3357,6 +3474,12 @@ msgstr "" | ||||
| "Wanneer ingeschakeld, slaagt de stap en gaat verder wanneer ongeldige " | ||||
| "gebruikersgegevens zijn ingevoerd." | ||||
|  | ||||
| #: authentik/stages/identification/models.py | ||||
| msgid "" | ||||
| "Show the user the 'Remember me on this device' toggle, allowing repeat users" | ||||
| " to skip straight to entering their password." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/stages/identification/models.py | ||||
| msgid "Optional enrollment flow, which is linked at the bottom of the page." | ||||
| msgstr "Optionele inschrijvingsflow, die onderaan de pagina is gekoppeld." | ||||
| @ -3742,6 +3865,14 @@ msgstr "" | ||||
| "Gebeurtenissen worden verwijderd na deze duur. (Indeling: " | ||||
| "weken=3;dagen=2;uren=3;seconden=2)." | ||||
|  | ||||
| #: authentik/tenants/models.py | ||||
| msgid "Reputation cannot decrease lower than this value. Zero or negative." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/tenants/models.py | ||||
| msgid "Reputation cannot increase higher than this value. Zero or positive." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/tenants/models.py | ||||
| msgid "The option configures the footer links on the flow executor pages." | ||||
| msgstr "De optie stelt de voettekst links in op de flow uitvoer pagina's." | ||||
|  | ||||
| @ -11,7 +11,7 @@ msgid "" | ||||
| msgstr "" | ||||
| "Project-Id-Version: PACKAGE VERSION\n" | ||||
| "Report-Msgid-Bugs-To: \n" | ||||
| "POT-Creation-Date: 2025-04-23 09:00+0000\n" | ||||
| "POT-Creation-Date: 2025-05-28 11:25+0000\n" | ||||
| "PO-Revision-Date: 2022-09-26 16:47+0000\n" | ||||
| "Last-Translator: Hugo Bicho, 2025\n" | ||||
| "Language-Team: Portuguese (https://app.transifex.com/authentik/teams/119923/pt/)\n" | ||||
| @ -105,6 +105,10 @@ msgstr "" | ||||
| msgid "Web Certificate used by the authentik Core webserver." | ||||
| msgstr "Certificado Web usado pelo servidor web authentik Core." | ||||
|  | ||||
| #: authentik/brands/models.py | ||||
| msgid "Certificates used for client authentication." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/brands/models.py | ||||
| msgid "Brand" | ||||
| msgstr "Marca" | ||||
| @ -662,6 +666,33 @@ msgstr "Dispositivos do ponto de ligação" | ||||
| msgid "Verifying your browser..." | ||||
| msgstr "A verificar o seu browser..." | ||||
|  | ||||
| #: authentik/enterprise/stages/mtls/models.py | ||||
| msgid "" | ||||
| "Configure certificate authorities to validate the certificate against. This " | ||||
| "option has a higher priority than the `client_certificate` option on " | ||||
| "`Brand`." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/stages/mtls/models.py | ||||
| msgid "Mutual TLS Stage" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/stages/mtls/models.py | ||||
| msgid "Mutual TLS Stages" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/stages/mtls/models.py | ||||
| msgid "Permissions to pass Certificates for outposts." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/stages/mtls/stage.py | ||||
| msgid "Certificate required but no certificate was given." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/stages/mtls/stage.py | ||||
| msgid "No user found for certificate." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/stages/source/models.py | ||||
| msgid "" | ||||
| "Amount of time a user can take to return from the source to continue the " | ||||
| @ -1007,9 +1038,12 @@ msgid "Starting full provider sync" | ||||
| msgstr "Iniciando a sincronização completa com o provedor" | ||||
|  | ||||
| #: authentik/lib/sync/outgoing/tasks.py | ||||
| #, python-brace-format | ||||
| msgid "Syncing page {page} of users" | ||||
| msgstr "A sincronizar a página {page} dos utilizadores" | ||||
| msgid "Syncing users" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/lib/sync/outgoing/tasks.py | ||||
| msgid "Syncing groups" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/lib/sync/outgoing/tasks.py | ||||
| #, python-brace-format | ||||
| @ -2456,6 +2490,12 @@ msgstr "" | ||||
| " um atributo do grupo. Isto permite a resolução de grupos hierárquicos em " | ||||
| "sistemas como o FreeIPA e Active Directory." | ||||
|  | ||||
| #: authentik/sources/ldap/models.py | ||||
| msgid "" | ||||
| "Delete authentik users and groups which were previously supplied by this " | ||||
| "source, but are now missing from it." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/sources/ldap/models.py | ||||
| msgid "LDAP Source" | ||||
| msgstr "Fonte LDAP" | ||||
| @ -2472,6 +2512,11 @@ msgstr "Mapeamento de propriedades de fonte LDAP" | ||||
| msgid "LDAP Source Property Mappings" | ||||
| msgstr "Mapeamentos de propriedades de fonte LDAP" | ||||
|  | ||||
| #: authentik/sources/ldap/models.py | ||||
| msgid "" | ||||
| "Unique ID used while checking if this object still exists in the directory." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/sources/ldap/models.py | ||||
| msgid "User LDAP Source Connection" | ||||
| msgstr "Ligação à fonte LDAP de Utilizador" | ||||
| @ -2865,6 +2910,11 @@ msgstr "Ligação à fonte SAML de Grupo" | ||||
| msgid "Group SAML Source Connections" | ||||
| msgstr "Ligações à fonte SAML de Grupo" | ||||
|  | ||||
| #: authentik/sources/saml/views.py | ||||
| #, python-brace-format | ||||
| msgid "Continue to {source_name}" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/sources/scim/models.py | ||||
| msgid "SCIM Source" | ||||
| msgstr "Fonte SCIM" | ||||
| @ -3255,6 +3305,10 @@ msgstr "Consentimento do Utilizador" | ||||
| msgid "User Consents" | ||||
| msgstr "Consentimentos do Utilizador" | ||||
|  | ||||
| #: authentik/stages/consent/stage.py | ||||
| msgid "Invalid consent token, re-showing prompt" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/stages/deny/models.py | ||||
| msgid "Deny Stage" | ||||
| msgstr "Etapa de negação" | ||||
| @ -3271,6 +3325,14 @@ msgstr "Etapa fictícia" | ||||
| msgid "Dummy Stages" | ||||
| msgstr "Etapas fictícias" | ||||
|  | ||||
| #: authentik/stages/email/flow.py | ||||
| msgid "Continue to confirm this email address." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/stages/email/flow.py | ||||
| msgid "Link was already used, please request a new link." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/stages/email/models.py | ||||
| msgid "Password Reset" | ||||
| msgstr "Redefinição de Palavra-Passe" | ||||
|  | ||||
										
											Binary file not shown.
										
									
								
							| @ -8,19 +8,19 @@ | ||||
| # Josenivaldo Benito Junior, 2023 | ||||
| # Caio Lima, 2023 | ||||
| # Hacklab, 2023 | ||||
| # Wagner Santos, 2024 | ||||
| # Rafael Mundel, 2024 | ||||
| # Anderson Silva Andrade <anderson.asa89@gmail.com>, 2025 | ||||
| # Gil Poiares-Oliveira, 2025 | ||||
| # Wagner Santos, 2025 | ||||
| #  | ||||
| #, fuzzy | ||||
| msgid "" | ||||
| msgstr "" | ||||
| "Project-Id-Version: PACKAGE VERSION\n" | ||||
| "Report-Msgid-Bugs-To: \n" | ||||
| "POT-Creation-Date: 2025-04-23 09:00+0000\n" | ||||
| "POT-Creation-Date: 2025-05-28 11:25+0000\n" | ||||
| "PO-Revision-Date: 2022-09-26 16:47+0000\n" | ||||
| "Last-Translator: Gil Poiares-Oliveira, 2025\n" | ||||
| "Last-Translator: Wagner Santos, 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" | ||||
| @ -112,6 +112,10 @@ msgstr "" | ||||
| msgid "Web Certificate used by the authentik Core webserver." | ||||
| msgstr "Certificado da Web usado pelo servidor da web authentik Core." | ||||
|  | ||||
| #: authentik/brands/models.py | ||||
| msgid "Certificates used for client authentication." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/brands/models.py | ||||
| msgid "Brand" | ||||
| msgstr "Brand" | ||||
| @ -271,11 +275,11 @@ msgstr "Aplicativos" | ||||
|  | ||||
| #: authentik/core/models.py | ||||
| msgid "Application Entitlement" | ||||
| msgstr "" | ||||
| msgstr "Autorização de aplicação" | ||||
|  | ||||
| #: authentik/core/models.py | ||||
| msgid "Application Entitlements" | ||||
| msgstr "" | ||||
| msgstr "Autorizações de aplicação" | ||||
|  | ||||
| #: authentik/core/models.py | ||||
| msgid "Use the source-specific identifier" | ||||
| @ -379,15 +383,15 @@ msgstr "Mapeamentos de propriedades" | ||||
|  | ||||
| #: authentik/core/models.py | ||||
| msgid "session data" | ||||
| msgstr "" | ||||
| msgstr "dados de sessão" | ||||
|  | ||||
| #: authentik/core/models.py | ||||
| msgid "Session" | ||||
| msgstr "" | ||||
| msgstr "Sessão" | ||||
|  | ||||
| #: authentik/core/models.py | ||||
| msgid "Sessions" | ||||
| msgstr "" | ||||
| msgstr "Sessões" | ||||
|  | ||||
| #: authentik/core/models.py | ||||
| msgid "Authenticated Session" | ||||
| @ -505,7 +509,7 @@ msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/policies/unique_password/models.py | ||||
| msgid "Number of passwords to check against." | ||||
| msgstr "" | ||||
| msgstr "Número de senhas para verificar." | ||||
|  | ||||
| #: authentik/enterprise/policies/unique_password/models.py | ||||
| #: authentik/policies/password/models.py | ||||
| @ -514,19 +518,19 @@ msgstr "Senha não definida no contexto" | ||||
|  | ||||
| #: authentik/enterprise/policies/unique_password/models.py | ||||
| msgid "This password has been used previously. Please choose a different one." | ||||
| msgstr "" | ||||
| msgstr "A senha já foi utilizada antes. Por favor, escolha uma diferente." | ||||
|  | ||||
| #: authentik/enterprise/policies/unique_password/models.py | ||||
| msgid "Password Uniqueness Policy" | ||||
| msgstr "" | ||||
| msgstr "Política de exclusividade de senha" | ||||
|  | ||||
| #: authentik/enterprise/policies/unique_password/models.py | ||||
| msgid "Password Uniqueness Policies" | ||||
| msgstr "" | ||||
| msgstr "Políticas de exclusividade de senha" | ||||
|  | ||||
| #: authentik/enterprise/policies/unique_password/models.py | ||||
| msgid "User Password History" | ||||
| msgstr "" | ||||
| msgstr "Histórico de senhas do usuário" | ||||
|  | ||||
| #: authentik/enterprise/policy.py | ||||
| msgid "Enterprise required to access this feature." | ||||
| @ -610,39 +614,39 @@ msgstr "Chave de Assinatura" | ||||
|  | ||||
| #: authentik/enterprise/providers/ssf/models.py | ||||
| msgid "Key used to sign the SSF Events." | ||||
| msgstr "" | ||||
| msgstr "Chave utilizada para assinar os eventos SSF." | ||||
|  | ||||
| #: authentik/enterprise/providers/ssf/models.py | ||||
| msgid "Shared Signals Framework Provider" | ||||
| msgstr "" | ||||
| msgstr "Provedor de Shared Signals Framework" | ||||
|  | ||||
| #: authentik/enterprise/providers/ssf/models.py | ||||
| msgid "Shared Signals Framework Providers" | ||||
| msgstr "" | ||||
| msgstr "Provedores de Shared Signals Framework" | ||||
|  | ||||
| #: authentik/enterprise/providers/ssf/models.py | ||||
| msgid "Add stream to SSF provider" | ||||
| msgstr "" | ||||
| msgstr "Adicionar stream ao fornecedor SSF" | ||||
|  | ||||
| #: authentik/enterprise/providers/ssf/models.py | ||||
| msgid "SSF Stream" | ||||
| msgstr "" | ||||
| msgstr "Stream SSF" | ||||
|  | ||||
| #: authentik/enterprise/providers/ssf/models.py | ||||
| msgid "SSF Streams" | ||||
| msgstr "" | ||||
| msgstr "Streams SSF" | ||||
|  | ||||
| #: authentik/enterprise/providers/ssf/models.py | ||||
| msgid "SSF Stream Event" | ||||
| msgstr "" | ||||
| msgstr "Evento de stream SSF" | ||||
|  | ||||
| #: authentik/enterprise/providers/ssf/models.py | ||||
| msgid "SSF Stream Events" | ||||
| msgstr "" | ||||
| msgstr "Eventos de stream SSF" | ||||
|  | ||||
| #: authentik/enterprise/providers/ssf/tasks.py | ||||
| msgid "Failed to send request" | ||||
| msgstr "" | ||||
| msgstr "Falha ao enviar requisição" | ||||
|  | ||||
| #: authentik/enterprise/stages/authenticator_endpoint_gdtc/models.py | ||||
| msgid "Endpoint Authenticator Google Device Trust Connector Stage" | ||||
| @ -664,6 +668,33 @@ msgstr "" | ||||
| msgid "Verifying your browser..." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/stages/mtls/models.py | ||||
| msgid "" | ||||
| "Configure certificate authorities to validate the certificate against. This " | ||||
| "option has a higher priority than the `client_certificate` option on " | ||||
| "`Brand`." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/stages/mtls/models.py | ||||
| msgid "Mutual TLS Stage" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/stages/mtls/models.py | ||||
| msgid "Mutual TLS Stages" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/stages/mtls/models.py | ||||
| msgid "Permissions to pass Certificates for outposts." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/stages/mtls/stage.py | ||||
| msgid "Certificate required but no certificate was given." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/stages/mtls/stage.py | ||||
| msgid "No user found for certificate." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/stages/source/models.py | ||||
| msgid "" | ||||
| "Amount of time a user can take to return from the source to continue the " | ||||
| @ -681,7 +712,7 @@ msgstr "" | ||||
| #: authentik/events/api/tasks.py | ||||
| #, python-brace-format | ||||
| msgid "Successfully started task {name}." | ||||
| msgstr "" | ||||
| msgstr "Tarefa {name} iniciada com sucesso." | ||||
|  | ||||
| #: authentik/events/models.py | ||||
| msgid "Event" | ||||
| @ -713,12 +744,16 @@ msgid "" | ||||
| "Customize the body of the request. Mapping should return data that is JSON-" | ||||
| "serializable." | ||||
| msgstr "" | ||||
| "Personalize o corpo do pedido. O mapeamento deve retornar dados que sejam " | ||||
| "serializáveis em JSON." | ||||
|  | ||||
| #: authentik/events/models.py | ||||
| msgid "" | ||||
| "Configure additional headers to be sent. Mapping should return a dictionary " | ||||
| "of key-value pairs" | ||||
| msgstr "" | ||||
| "Configurar cabeçalhos adicionais a serem enviados. O mapeamento deve " | ||||
| "retornar um dicionário de pares chave-valor" | ||||
|  | ||||
| #: authentik/events/models.py | ||||
| msgid "" | ||||
| @ -998,8 +1033,11 @@ msgid "Starting full provider sync" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/lib/sync/outgoing/tasks.py | ||||
| #, python-brace-format | ||||
| msgid "Syncing page {page} of users" | ||||
| msgid "Syncing users" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/lib/sync/outgoing/tasks.py | ||||
| msgid "Syncing groups" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/lib/sync/outgoing/tasks.py | ||||
| @ -1314,7 +1352,7 @@ msgstr "" | ||||
| #: authentik/policies/password/models.py | ||||
| #, python-brace-format | ||||
| msgid "Password exists on {count} online lists." | ||||
| msgstr "" | ||||
| msgstr "A senha está presente em {count}  listas de senhas vulneráveis." | ||||
|  | ||||
| #: authentik/policies/password/models.py | ||||
| msgid "Password is too weak." | ||||
| @ -2396,6 +2434,12 @@ msgid "" | ||||
| "Active Directory" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/sources/ldap/models.py | ||||
| msgid "" | ||||
| "Delete authentik users and groups which were previously supplied by this " | ||||
| "source, but are now missing from it." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/sources/ldap/models.py | ||||
| msgid "LDAP Source" | ||||
| msgstr "Fonte LDAP" | ||||
| @ -2412,6 +2456,11 @@ msgstr "" | ||||
| msgid "LDAP Source Property Mappings" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/sources/ldap/models.py | ||||
| msgid "" | ||||
| "Unique ID used while checking if this object still exists in the directory." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/sources/ldap/models.py | ||||
| msgid "User LDAP Source Connection" | ||||
| msgstr "" | ||||
| @ -2802,6 +2851,11 @@ msgstr "" | ||||
| msgid "Group SAML Source Connections" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/sources/saml/views.py | ||||
| #, python-brace-format | ||||
| msgid "Continue to {source_name}" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/sources/scim/models.py | ||||
| msgid "SCIM Source" | ||||
| msgstr "" | ||||
| @ -3174,6 +3228,10 @@ msgstr "Consentimento do usuário" | ||||
| msgid "User Consents" | ||||
| msgstr "Consentimentos do usuário" | ||||
|  | ||||
| #: authentik/stages/consent/stage.py | ||||
| msgid "Invalid consent token, re-showing prompt" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/stages/deny/models.py | ||||
| msgid "Deny Stage" | ||||
| msgstr "Negar Estágio" | ||||
| @ -3190,6 +3248,14 @@ msgstr "Palco fictício" | ||||
| msgid "Dummy Stages" | ||||
| msgstr "Fases fictícias" | ||||
|  | ||||
| #: authentik/stages/email/flow.py | ||||
| msgid "Continue to confirm this email address." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/stages/email/flow.py | ||||
| msgid "Link was already used, please request a new link." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/stages/email/models.py | ||||
| msgid "Password Reset" | ||||
| msgstr "Redefinição de senha" | ||||
|  | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -18,7 +18,7 @@ msgid "" | ||||
| msgstr "" | ||||
| "Project-Id-Version: PACKAGE VERSION\n" | ||||
| "Report-Msgid-Bugs-To: \n" | ||||
| "POT-Creation-Date: 2025-04-23 09:00+0000\n" | ||||
| "POT-Creation-Date: 2025-05-28 11:25+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" | ||||
| @ -111,6 +111,10 @@ msgstr "" | ||||
| msgid "Web Certificate used by the authentik Core webserver." | ||||
| msgstr "Web Certificate используемый для authentik Core webserver." | ||||
|  | ||||
| #: authentik/brands/models.py | ||||
| msgid "Certificates used for client authentication." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/brands/models.py | ||||
| msgid "Brand" | ||||
| msgstr "Бренд" | ||||
| @ -669,6 +673,33 @@ msgstr "Конечные устройства" | ||||
| msgid "Verifying your browser..." | ||||
| msgstr "Проверка вашего браузера..." | ||||
|  | ||||
| #: authentik/enterprise/stages/mtls/models.py | ||||
| msgid "" | ||||
| "Configure certificate authorities to validate the certificate against. This " | ||||
| "option has a higher priority than the `client_certificate` option on " | ||||
| "`Brand`." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/stages/mtls/models.py | ||||
| msgid "Mutual TLS Stage" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/stages/mtls/models.py | ||||
| msgid "Mutual TLS Stages" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/stages/mtls/models.py | ||||
| msgid "Permissions to pass Certificates for outposts." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/stages/mtls/stage.py | ||||
| msgid "Certificate required but no certificate was given." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/stages/mtls/stage.py | ||||
| msgid "No user found for certificate." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/stages/source/models.py | ||||
| msgid "" | ||||
| "Amount of time a user can take to return from the source to continue the " | ||||
| @ -1009,8 +1040,11 @@ msgid "Starting full provider sync" | ||||
| msgstr "Запуск полной синхронизации провайдера" | ||||
|  | ||||
| #: authentik/lib/sync/outgoing/tasks.py | ||||
| #, python-brace-format | ||||
| msgid "Syncing page {page} of users" | ||||
| msgid "Syncing users" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/lib/sync/outgoing/tasks.py | ||||
| msgid "Syncing groups" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/lib/sync/outgoing/tasks.py | ||||
| @ -2430,6 +2464,12 @@ msgid "" | ||||
| "Active Directory" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/sources/ldap/models.py | ||||
| msgid "" | ||||
| "Delete authentik users and groups which were previously supplied by this " | ||||
| "source, but are now missing from it." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/sources/ldap/models.py | ||||
| msgid "LDAP Source" | ||||
| msgstr "Источник LDAP" | ||||
| @ -2446,6 +2486,11 @@ msgstr "Сопоставление свойства LDAP источника" | ||||
| msgid "LDAP Source Property Mappings" | ||||
| msgstr "Сопоставление свойств LDAP источника" | ||||
|  | ||||
| #: authentik/sources/ldap/models.py | ||||
| msgid "" | ||||
| "Unique ID used while checking if this object still exists in the directory." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/sources/ldap/models.py | ||||
| msgid "User LDAP Source Connection" | ||||
| msgstr "" | ||||
| @ -2842,6 +2887,11 @@ msgstr "Групповое подключение к источнику SAML" | ||||
| msgid "Group SAML Source Connections" | ||||
| msgstr "Групповые подключения к источнику SAML" | ||||
|  | ||||
| #: authentik/sources/saml/views.py | ||||
| #, python-brace-format | ||||
| msgid "Continue to {source_name}" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/sources/scim/models.py | ||||
| msgid "SCIM Source" | ||||
| msgstr "Источник SCIM" | ||||
| @ -3219,6 +3269,10 @@ msgstr "Согласие пользователя" | ||||
| msgid "User Consents" | ||||
| msgstr "Согласия пользователя" | ||||
|  | ||||
| #: authentik/stages/consent/stage.py | ||||
| msgid "Invalid consent token, re-showing prompt" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/stages/deny/models.py | ||||
| msgid "Deny Stage" | ||||
| msgstr "Этап отказа" | ||||
| @ -3235,6 +3289,14 @@ msgstr "Фиктивный этап" | ||||
| msgid "Dummy Stages" | ||||
| msgstr "Фиктивные этапы" | ||||
|  | ||||
| #: authentik/stages/email/flow.py | ||||
| msgid "Continue to confirm this email address." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/stages/email/flow.py | ||||
| msgid "Link was already used, please request a new link." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/stages/email/models.py | ||||
| msgid "Password Reset" | ||||
| msgstr "Сброс пароля" | ||||
|  | ||||
| @ -13,7 +13,7 @@ msgid "" | ||||
| msgstr "" | ||||
| "Project-Id-Version: PACKAGE VERSION\n" | ||||
| "Report-Msgid-Bugs-To: \n" | ||||
| "POT-Creation-Date: 2025-04-23 09:00+0000\n" | ||||
| "POT-Creation-Date: 2025-05-28 11:25+0000\n" | ||||
| "PO-Revision-Date: 2022-09-26 16:47+0000\n" | ||||
| "Last-Translator: Jens L. <jens@goauthentik.io>, 2025\n" | ||||
| "Language-Team: Turkish (https://app.transifex.com/authentik/teams/119923/tr/)\n" | ||||
| @ -107,6 +107,10 @@ msgstr "" | ||||
| msgid "Web Certificate used by the authentik Core webserver." | ||||
| msgstr "Authentik Core web sunucusu tarafından kullanılan Web Sertifikası." | ||||
|  | ||||
| #: authentik/brands/models.py | ||||
| msgid "Certificates used for client authentication." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/brands/models.py | ||||
| msgid "Brand" | ||||
| msgstr "Marka" | ||||
| @ -659,6 +663,33 @@ msgstr "Uç Nokta Cihazları" | ||||
| msgid "Verifying your browser..." | ||||
| msgstr "Tarayıcınız doğrulanıyor..." | ||||
|  | ||||
| #: authentik/enterprise/stages/mtls/models.py | ||||
| msgid "" | ||||
| "Configure certificate authorities to validate the certificate against. This " | ||||
| "option has a higher priority than the `client_certificate` option on " | ||||
| "`Brand`." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/stages/mtls/models.py | ||||
| msgid "Mutual TLS Stage" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/stages/mtls/models.py | ||||
| msgid "Mutual TLS Stages" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/stages/mtls/models.py | ||||
| msgid "Permissions to pass Certificates for outposts." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/stages/mtls/stage.py | ||||
| msgid "Certificate required but no certificate was given." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/stages/mtls/stage.py | ||||
| msgid "No user found for certificate." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/stages/source/models.py | ||||
| msgid "" | ||||
| "Amount of time a user can take to return from the source to continue the " | ||||
| @ -1000,8 +1031,11 @@ msgid "Starting full provider sync" | ||||
| msgstr "Tam sağlayıcı senkronizasyonunu başlatma" | ||||
|  | ||||
| #: authentik/lib/sync/outgoing/tasks.py | ||||
| #, python-brace-format | ||||
| msgid "Syncing page {page} of users" | ||||
| msgid "Syncing users" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/lib/sync/outgoing/tasks.py | ||||
| msgid "Syncing groups" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/lib/sync/outgoing/tasks.py | ||||
| @ -2430,6 +2464,12 @@ msgid "" | ||||
| "Active Directory" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/sources/ldap/models.py | ||||
| msgid "" | ||||
| "Delete authentik users and groups which were previously supplied by this " | ||||
| "source, but are now missing from it." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/sources/ldap/models.py | ||||
| msgid "LDAP Source" | ||||
| msgstr "LDAP Kaynağı" | ||||
| @ -2446,6 +2486,11 @@ msgstr "LDAP Kaynak Özellik Eşlemesi" | ||||
| msgid "LDAP Source Property Mappings" | ||||
| msgstr "LDAP Kaynak Özellik Eşlemeleri" | ||||
|  | ||||
| #: authentik/sources/ldap/models.py | ||||
| msgid "" | ||||
| "Unique ID used while checking if this object still exists in the directory." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/sources/ldap/models.py | ||||
| msgid "User LDAP Source Connection" | ||||
| msgstr "" | ||||
| @ -2837,6 +2882,11 @@ msgstr "Grup SAML Kaynak Bağlantısı" | ||||
| msgid "Group SAML Source Connections" | ||||
| msgstr "Grup SAML Kaynak Bağlantıları" | ||||
|  | ||||
| #: authentik/sources/saml/views.py | ||||
| #, python-brace-format | ||||
| msgid "Continue to {source_name}" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/sources/scim/models.py | ||||
| msgid "SCIM Source" | ||||
| msgstr "SCIM Kaynak" | ||||
| @ -3211,6 +3261,10 @@ msgstr "Kullanıcı Onayı" | ||||
| msgid "User Consents" | ||||
| msgstr "Kullanıcı Onayları" | ||||
|  | ||||
| #: authentik/stages/consent/stage.py | ||||
| msgid "Invalid consent token, re-showing prompt" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/stages/deny/models.py | ||||
| msgid "Deny Stage" | ||||
| msgstr "Aşama Alanını Reddet" | ||||
| @ -3227,6 +3281,14 @@ msgstr "Kukla Aşaması" | ||||
| msgid "Dummy Stages" | ||||
| msgstr "Kukla Aşamaları" | ||||
|  | ||||
| #: authentik/stages/email/flow.py | ||||
| msgid "Continue to confirm this email address." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/stages/email/flow.py | ||||
| msgid "Link was already used, please request a new link." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/stages/email/models.py | ||||
| msgid "Password Reset" | ||||
| msgstr "Parola Sıfırlama" | ||||
|  | ||||
| @ -15,7 +15,7 @@ msgid "" | ||||
| msgstr "" | ||||
| "Project-Id-Version: PACKAGE VERSION\n" | ||||
| "Report-Msgid-Bugs-To: \n" | ||||
| "POT-Creation-Date: 2025-05-20 00:10+0000\n" | ||||
| "POT-Creation-Date: 2025-05-28 11:25+0000\n" | ||||
| "PO-Revision-Date: 2022-09-26 16:47+0000\n" | ||||
| "Last-Translator: deluxghost, 2025\n" | ||||
| "Language-Team: Chinese Simplified (https://app.transifex.com/authentik/teams/119923/zh-Hans/)\n" | ||||
| @ -975,9 +975,12 @@ msgid "Starting full provider sync" | ||||
| msgstr "开始全量提供程序同步" | ||||
|  | ||||
| #: authentik/lib/sync/outgoing/tasks.py | ||||
| #, python-brace-format | ||||
| msgid "Syncing page {page} of users" | ||||
| msgstr "正在同步用户页面 {page}" | ||||
| msgid "Syncing users" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/lib/sync/outgoing/tasks.py | ||||
| msgid "Syncing groups" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/lib/sync/outgoing/tasks.py | ||||
| #, python-brace-format | ||||
| @ -2285,6 +2288,12 @@ msgid "" | ||||
| "Active Directory" | ||||
| msgstr "基于用户属性而非组属性查询组成员身份。这允许在 FreeIPA 或 Active Directory 等系统上支持嵌套组决策" | ||||
|  | ||||
| #: authentik/sources/ldap/models.py | ||||
| msgid "" | ||||
| "Delete authentik users and groups which were previously supplied by this " | ||||
| "source, but are now missing from it." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/sources/ldap/models.py | ||||
| msgid "LDAP Source" | ||||
| msgstr "LDAP 源" | ||||
| @ -2301,6 +2310,11 @@ msgstr "LDAP 源属性映射" | ||||
| msgid "LDAP Source Property Mappings" | ||||
| msgstr "LDAP 源属性映射" | ||||
|  | ||||
| #: authentik/sources/ldap/models.py | ||||
| msgid "" | ||||
| "Unique ID used while checking if this object still exists in the directory." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/sources/ldap/models.py | ||||
| msgid "User LDAP Source Connection" | ||||
| msgstr "用户 LDAP 源连接" | ||||
| @ -2678,6 +2692,11 @@ msgstr "组 SAML 源连接" | ||||
| msgid "Group SAML Source Connections" | ||||
| msgstr "组 SAML 源连接" | ||||
|  | ||||
| #: authentik/sources/saml/views.py | ||||
| #, python-brace-format | ||||
| msgid "Continue to {source_name}" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/sources/scim/models.py | ||||
| msgid "SCIM Source" | ||||
| msgstr "SCIM 源" | ||||
| @ -3044,6 +3063,10 @@ msgstr "用户同意授权" | ||||
| msgid "User Consents" | ||||
| msgstr "用户同意授权" | ||||
|  | ||||
| #: authentik/stages/consent/stage.py | ||||
| msgid "Invalid consent token, re-showing prompt" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/stages/deny/models.py | ||||
| msgid "Deny Stage" | ||||
| msgstr "拒绝阶段" | ||||
| @ -3060,6 +3083,14 @@ msgstr "虚拟阶段" | ||||
| msgid "Dummy Stages" | ||||
| msgstr "虚拟阶段" | ||||
|  | ||||
| #: authentik/stages/email/flow.py | ||||
| msgid "Continue to confirm this email address." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/stages/email/flow.py | ||||
| msgid "Link was already used, please request a new link." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/stages/email/models.py | ||||
| msgid "Password Reset" | ||||
| msgstr "密码重置" | ||||
|  | ||||
										
											Binary file not shown.
										
									
								
							| @ -14,7 +14,7 @@ msgid "" | ||||
| msgstr "" | ||||
| "Project-Id-Version: PACKAGE VERSION\n" | ||||
| "Report-Msgid-Bugs-To: \n" | ||||
| "POT-Creation-Date: 2025-05-20 00:10+0000\n" | ||||
| "POT-Creation-Date: 2025-05-28 11:25+0000\n" | ||||
| "PO-Revision-Date: 2022-09-26 16:47+0000\n" | ||||
| "Last-Translator: deluxghost, 2025\n" | ||||
| "Language-Team: Chinese (China) (https://app.transifex.com/authentik/teams/119923/zh_CN/)\n" | ||||
| @ -974,9 +974,12 @@ msgid "Starting full provider sync" | ||||
| msgstr "开始全量提供程序同步" | ||||
|  | ||||
| #: authentik/lib/sync/outgoing/tasks.py | ||||
| #, python-brace-format | ||||
| msgid "Syncing page {page} of users" | ||||
| msgstr "正在同步用户页面 {page}" | ||||
| msgid "Syncing users" | ||||
| msgstr "正在同步用户" | ||||
|  | ||||
| #: authentik/lib/sync/outgoing/tasks.py | ||||
| msgid "Syncing groups" | ||||
| msgstr "正在同步组" | ||||
|  | ||||
| #: authentik/lib/sync/outgoing/tasks.py | ||||
| #, python-brace-format | ||||
| @ -2284,6 +2287,12 @@ msgid "" | ||||
| "Active Directory" | ||||
| msgstr "基于用户属性而非组属性查询组成员身份。这允许在 FreeIPA 或 Active Directory 等系统上支持嵌套组决策" | ||||
|  | ||||
| #: authentik/sources/ldap/models.py | ||||
| msgid "" | ||||
| "Delete authentik users and groups which were previously supplied by this " | ||||
| "source, but are now missing from it." | ||||
| msgstr "删除之前由此源提供,但现已缺失的用户和组。" | ||||
|  | ||||
| #: authentik/sources/ldap/models.py | ||||
| msgid "LDAP Source" | ||||
| msgstr "LDAP 源" | ||||
| @ -2300,6 +2309,11 @@ msgstr "LDAP 源属性映射" | ||||
| msgid "LDAP Source Property Mappings" | ||||
| msgstr "LDAP 源属性映射" | ||||
|  | ||||
| #: authentik/sources/ldap/models.py | ||||
| msgid "" | ||||
| "Unique ID used while checking if this object still exists in the directory." | ||||
| msgstr "检查此对象是否仍在目录中时使用的唯一 ID。" | ||||
|  | ||||
| #: authentik/sources/ldap/models.py | ||||
| msgid "User LDAP Source Connection" | ||||
| msgstr "用户 LDAP 源连接" | ||||
| @ -2677,6 +2691,11 @@ msgstr "组 SAML 源连接" | ||||
| msgid "Group SAML Source Connections" | ||||
| msgstr "组 SAML 源连接" | ||||
|  | ||||
| #: authentik/sources/saml/views.py | ||||
| #, python-brace-format | ||||
| msgid "Continue to {source_name}" | ||||
| msgstr "继续前往 {source_name}" | ||||
|  | ||||
| #: authentik/sources/scim/models.py | ||||
| msgid "SCIM Source" | ||||
| msgstr "SCIM 源" | ||||
| @ -3043,6 +3062,10 @@ msgstr "用户同意授权" | ||||
| msgid "User Consents" | ||||
| msgstr "用户同意授权" | ||||
|  | ||||
| #: authentik/stages/consent/stage.py | ||||
| msgid "Invalid consent token, re-showing prompt" | ||||
| msgstr "无效的同意令牌,将重新显示输入" | ||||
|  | ||||
| #: authentik/stages/deny/models.py | ||||
| msgid "Deny Stage" | ||||
| msgstr "拒绝阶段" | ||||
| @ -3059,6 +3082,14 @@ msgstr "虚拟阶段" | ||||
| msgid "Dummy Stages" | ||||
| msgstr "虚拟阶段" | ||||
|  | ||||
| #: authentik/stages/email/flow.py | ||||
| msgid "Continue to confirm this email address." | ||||
| msgstr "继续以确认电子邮件地址。" | ||||
|  | ||||
| #: authentik/stages/email/flow.py | ||||
| msgid "Link was already used, please request a new link." | ||||
| msgstr "链接已被使用,请申请一个新链接。" | ||||
|  | ||||
| #: authentik/stages/email/models.py | ||||
| msgid "Password Reset" | ||||
| msgstr "密码重置" | ||||
|  | ||||
| @ -14,7 +14,7 @@ msgid "" | ||||
| msgstr "" | ||||
| "Project-Id-Version: PACKAGE VERSION\n" | ||||
| "Report-Msgid-Bugs-To: \n" | ||||
| "POT-Creation-Date: 2025-04-23 09:00+0000\n" | ||||
| "POT-Creation-Date: 2025-05-28 11:25+0000\n" | ||||
| "PO-Revision-Date: 2022-09-26 16:47+0000\n" | ||||
| "Last-Translator: 刘松, 2025\n" | ||||
| "Language-Team: Chinese (Taiwan) (https://app.transifex.com/authentik/teams/119923/zh_TW/)\n" | ||||
| @ -101,6 +101,10 @@ msgstr "" | ||||
| msgid "Web Certificate used by the authentik Core webserver." | ||||
| msgstr "用於 authentik Core 網頁伺服器的網頁憑證。" | ||||
|  | ||||
| #: authentik/brands/models.py | ||||
| msgid "Certificates used for client authentication." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/brands/models.py | ||||
| msgid "Brand" | ||||
| msgstr "品牌" | ||||
| @ -625,6 +629,33 @@ msgstr "" | ||||
| msgid "Verifying your browser..." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/stages/mtls/models.py | ||||
| msgid "" | ||||
| "Configure certificate authorities to validate the certificate against. This " | ||||
| "option has a higher priority than the `client_certificate` option on " | ||||
| "`Brand`." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/stages/mtls/models.py | ||||
| msgid "Mutual TLS Stage" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/stages/mtls/models.py | ||||
| msgid "Mutual TLS Stages" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/stages/mtls/models.py | ||||
| msgid "Permissions to pass Certificates for outposts." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/stages/mtls/stage.py | ||||
| msgid "Certificate required but no certificate was given." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/stages/mtls/stage.py | ||||
| msgid "No user found for certificate." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/stages/source/models.py | ||||
| msgid "" | ||||
| "Amount of time a user can take to return from the source to continue the " | ||||
| @ -943,8 +974,11 @@ msgid "Starting full provider sync" | ||||
| msgstr "開始同步所有提供程式" | ||||
|  | ||||
| #: authentik/lib/sync/outgoing/tasks.py | ||||
| #, python-brace-format | ||||
| msgid "Syncing page {page} of users" | ||||
| msgid "Syncing users" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/lib/sync/outgoing/tasks.py | ||||
| msgid "Syncing groups" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/lib/sync/outgoing/tasks.py | ||||
| @ -2249,6 +2283,12 @@ msgid "" | ||||
| "Active Directory" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/sources/ldap/models.py | ||||
| msgid "" | ||||
| "Delete authentik users and groups which were previously supplied by this " | ||||
| "source, but are now missing from it." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/sources/ldap/models.py | ||||
| msgid "LDAP Source" | ||||
| msgstr "LDAP 來源" | ||||
| @ -2265,6 +2305,11 @@ msgstr "" | ||||
| msgid "LDAP Source Property Mappings" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/sources/ldap/models.py | ||||
| msgid "" | ||||
| "Unique ID used while checking if this object still exists in the directory." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/sources/ldap/models.py | ||||
| msgid "User LDAP Source Connection" | ||||
| msgstr "" | ||||
| @ -2642,6 +2687,11 @@ msgstr "" | ||||
| msgid "Group SAML Source Connections" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/sources/saml/views.py | ||||
| #, python-brace-format | ||||
| msgid "Continue to {source_name}" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/sources/scim/models.py | ||||
| msgid "SCIM Source" | ||||
| msgstr "SCIM 來源" | ||||
| @ -2998,6 +3048,10 @@ msgstr "使用者同意" | ||||
| msgid "User Consents" | ||||
| msgstr "使用者同意" | ||||
|  | ||||
| #: authentik/stages/consent/stage.py | ||||
| msgid "Invalid consent token, re-showing prompt" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/stages/deny/models.py | ||||
| msgid "Deny Stage" | ||||
| msgstr "拒絕階段" | ||||
| @ -3014,6 +3068,14 @@ msgstr "假階段" | ||||
| msgid "Dummy Stages" | ||||
| msgstr "假階段" | ||||
|  | ||||
| #: authentik/stages/email/flow.py | ||||
| msgid "Continue to confirm this email address." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/stages/email/flow.py | ||||
| msgid "Link was already used, please request a new link." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/stages/email/models.py | ||||
| msgid "Password Reset" | ||||
| msgstr "重設密碼" | ||||
|  | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user
	![gcp-cherry-pick-bot[bot]](/assets/img/avatar_default.png)