Compare commits
	
		
			155 Commits
		
	
	
		
			consistent
			...
			version/20
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| c6333f9e19 | |||
| 56565b0895 | |||
| cbbc7c1825 | |||
| 908aaa5afa | |||
| 937342eab1 | |||
| 82823a7449 | |||
| ad50f14a3e | |||
| e0cf6128df | |||
| bfbe8b8038 | |||
| 36ba8bc4e7 | |||
| dd5edf7fd9 | |||
| da1b252f3b | |||
| a8e543972a | |||
| 6e03045d1f | |||
| f4b39e7465 | |||
| e7cd5880b5 | |||
| d8c6a2417d | |||
| a1fe471a59 | |||
| 054dfda73f | |||
| 2e5e8f5c58 | |||
| c28b65a3f2 | |||
| afc9847e36 | |||
| 620c95dfa1 | |||
| 15c7a0a9be | 
| @ -1,5 +1,5 @@ | |||||||
| [bumpversion] | [bumpversion] | ||||||
| current_version = 2025.4.1 | current_version = 2025.6.3 | ||||||
| tag = True | tag = True | ||||||
| commit = True | commit = True | ||||||
| parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-(?P<rc_t>[a-zA-Z-]+)(?P<rc_n>[1-9]\\d*))? | parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-(?P<rc_t>[a-zA-Z-]+)(?P<rc_n>[1-9]\\d*))? | ||||||
| @ -21,6 +21,8 @@ optional_value = final | |||||||
|  |  | ||||||
| [bumpversion:file:package.json] | [bumpversion:file:package.json] | ||||||
|  |  | ||||||
|  | [bumpversion:file:package-lock.json] | ||||||
|  |  | ||||||
| [bumpversion:file:docker-compose.yml] | [bumpversion:file:docker-compose.yml] | ||||||
|  |  | ||||||
| [bumpversion:file:schema.yml] | [bumpversion:file:schema.yml] | ||||||
| @ -31,6 +33,4 @@ optional_value = final | |||||||
|  |  | ||||||
| [bumpversion:file:internal/constants/constants.go] | [bumpversion:file:internal/constants/constants.go] | ||||||
|  |  | ||||||
| [bumpversion:file:web/src/common/constants.ts] |  | ||||||
|  |  | ||||||
| [bumpversion:file:lifecycle/aws/template.yaml] | [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: |       with: | ||||||
|         go-version-file: "go.mod" |         go-version-file: "go.mod" | ||||||
|     - name: Setup docker cache |     - name: Setup docker cache | ||||||
|       uses: ScribeMD/docker-cache@0.5.0 |       uses: AndreKurait/docker-cache@0fe76702a40db986d9663c24954fc14c6a6031b7 | ||||||
|       with: |       with: | ||||||
|         key: docker-images-${{ runner.os }}-${{ hashFiles('.github/actions/setup/docker-compose.yml', 'Makefile') }}-${{ inputs.postgresql_version }} |         key: docker-images-${{ runner.os }}-${{ hashFiles('.github/actions/setup/docker-compose.yml', 'Makefile') }}-${{ inputs.postgresql_version }} | ||||||
|     - name: Setup dependencies |     - name: Setup dependencies | ||||||
|  | |||||||
							
								
								
									
										4
									
								
								.github/workflows/ci-main.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/ci-main.yml
									
									
									
									
										vendored
									
									
								
							| @ -62,6 +62,7 @@ jobs: | |||||||
|         psql: |         psql: | ||||||
|           - 15-alpine |           - 15-alpine | ||||||
|           - 16-alpine |           - 16-alpine | ||||||
|  |           - 17-alpine | ||||||
|         run_id: [1, 2, 3, 4, 5] |         run_id: [1, 2, 3, 4, 5] | ||||||
|     steps: |     steps: | ||||||
|       - uses: actions/checkout@v4 |       - uses: actions/checkout@v4 | ||||||
| @ -116,6 +117,7 @@ jobs: | |||||||
|         psql: |         psql: | ||||||
|           - 15-alpine |           - 15-alpine | ||||||
|           - 16-alpine |           - 16-alpine | ||||||
|  |           - 17-alpine | ||||||
|         run_id: [1, 2, 3, 4, 5] |         run_id: [1, 2, 3, 4, 5] | ||||||
|     steps: |     steps: | ||||||
|       - uses: actions/checkout@v4 |       - uses: actions/checkout@v4 | ||||||
| @ -200,7 +202,7 @@ jobs: | |||||||
|         uses: actions/cache@v4 |         uses: actions/cache@v4 | ||||||
|         with: |         with: | ||||||
|           path: web/dist |           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 |       - name: prepare web ui | ||||||
|         if: steps.cache-web.outputs.cache-hit != 'true' |         if: steps.cache-web.outputs.cache-hit != 'true' | ||||||
|         working-directory: web |         working-directory: web | ||||||
|  | |||||||
							
								
								
									
										1
									
								
								.github/workflows/ci-website.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/workflows/ci-website.yml
									
									
									
									
										vendored
									
									
								
							| @ -49,6 +49,7 @@ jobs: | |||||||
|       matrix: |       matrix: | ||||||
|         job: |         job: | ||||||
|           - build |           - build | ||||||
|  |           - build:integrations | ||||||
|     steps: |     steps: | ||||||
|       - uses: actions/checkout@v4 |       - uses: actions/checkout@v4 | ||||||
|       - uses: actions/setup-node@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: | on: | ||||||
|   push: |   push: | ||||||
|     branches: [main, "*", next, version*] |     branches: [main, next, version*] | ||||||
|   pull_request: |   pull_request: | ||||||
|     branches: [main] |     branches: [main] | ||||||
|   schedule: |   schedule: | ||||||
|  | |||||||
							
								
								
									
										17
									
								
								.github/workflows/packages-npm-publish.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										17
									
								
								.github/workflows/packages-npm-publish.yml
									
									
									
									
										vendored
									
									
								
							| @ -7,7 +7,7 @@ on: | |||||||
|       - packages/eslint-config/** |       - packages/eslint-config/** | ||||||
|       - packages/prettier-config/** |       - packages/prettier-config/** | ||||||
|       - packages/tsconfig/** |       - packages/tsconfig/** | ||||||
|       - packages/web/esbuild-plugin-live-reload/** |       - web/packages/esbuild-plugin-live-reload/** | ||||||
|   workflow_dispatch: |   workflow_dispatch: | ||||||
| jobs: | jobs: | ||||||
|   publish: |   publish: | ||||||
| @ -17,27 +17,28 @@ jobs: | |||||||
|       fail-fast: false |       fail-fast: false | ||||||
|       matrix: |       matrix: | ||||||
|         package: |         package: | ||||||
|           - docusaurus-config |           - packages/docusaurus-config | ||||||
|           - eslint-config |           - packages/eslint-config | ||||||
|           - prettier-config |           - packages/prettier-config | ||||||
|           - tsconfig |           - packages/tsconfig | ||||||
|  |           - web/packages/esbuild-plugin-live-reload | ||||||
|     steps: |     steps: | ||||||
|       - uses: actions/checkout@v4 |       - uses: actions/checkout@v4 | ||||||
|         with: |         with: | ||||||
|           fetch-depth: 2 |           fetch-depth: 2 | ||||||
|       - uses: actions/setup-node@v4 |       - uses: actions/setup-node@v4 | ||||||
|         with: |         with: | ||||||
|           node-version-file: packages/${{ matrix.package }}/package.json |           node-version-file: ${{ matrix.package }}/package.json | ||||||
|           registry-url: "https://registry.npmjs.org" |           registry-url: "https://registry.npmjs.org" | ||||||
|       - name: Get changed files |       - name: Get changed files | ||||||
|         id: changed-files |         id: changed-files | ||||||
|         uses: tj-actions/changed-files@ed68ef82c095e0d48ec87eccea555d944a631a4c |         uses: tj-actions/changed-files@ed68ef82c095e0d48ec87eccea555d944a631a4c | ||||||
|         with: |         with: | ||||||
|           files: | |           files: | | ||||||
|             packages/${{ matrix.package }}/package.json |             ${{ matrix.package }}/package.json | ||||||
|       - name: Publish package |       - name: Publish package | ||||||
|         if: steps.changed-files.outputs.any_changed == 'true' |         if: steps.changed-files.outputs.any_changed == 'true' | ||||||
|         working-directory: packages/${{ matrix.package}} |         working-directory: ${{ matrix.package }} | ||||||
|         run: | |         run: | | ||||||
|           npm ci |           npm ci | ||||||
|           npm run build |           npm run build | ||||||
|  | |||||||
| @ -94,9 +94,9 @@ RUN --mount=type=secret,id=GEOIPUPDATE_ACCOUNT_ID \ | |||||||
|     /bin/sh -c "GEOIPUPDATE_LICENSE_KEY_FILE=/run/secrets/GEOIPUPDATE_LICENSE_KEY /usr/bin/entry.sh || echo 'Failed to get GeoIP database, disabling'; exit 0" |     /bin/sh -c "GEOIPUPDATE_LICENSE_KEY_FILE=/run/secrets/GEOIPUPDATE_LICENSE_KEY /usr/bin/entry.sh || echo 'Failed to get GeoIP database, disabling'; exit 0" | ||||||
|  |  | ||||||
| # Stage 5: Download uv | # Stage 5: Download uv | ||||||
| FROM ghcr.io/astral-sh/uv:0.7.7 AS uv | FROM ghcr.io/astral-sh/uv:0.7.8 AS uv | ||||||
| # Stage 6: Base python image | # 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" \ | ENV VENV_PATH="/ak-root/.venv" \ | ||||||
|     PATH="/lifecycle:/ak-root/.venv/bin:$PATH" \ |     PATH="/lifecycle:/ak-root/.venv/bin:$PATH" \ | ||||||
|  | |||||||
							
								
								
									
										6
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										6
									
								
								Makefile
									
									
									
									
									
								
							| @ -1,6 +1,6 @@ | |||||||
| .PHONY: gen dev-reset all clean test web website | .PHONY: gen dev-reset all clean test web website | ||||||
|  |  | ||||||
| SHELL := /bin/bash | SHELL := /usr/bin/env bash | ||||||
| .SHELLFLAGS += ${SHELLFLAGS} -e -o pipefail | .SHELLFLAGS += ${SHELLFLAGS} -e -o pipefail | ||||||
| PWD = $(shell pwd) | PWD = $(shell pwd) | ||||||
| UID = $(shell id -u) | UID = $(shell id -u) | ||||||
| @ -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. | 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 | ## API Schema | ||||||
| ######################### | ######################### | ||||||
|  | |||||||
| @ -20,8 +20,8 @@ Even if the issue is not a CVE, we still greatly appreciate your help in hardeni | |||||||
|  |  | ||||||
| | Version   | Supported | | | Version   | Supported | | ||||||
| | --------- | --------- | | | --------- | --------- | | ||||||
| | 2025.2.x  | ✅        | |  | ||||||
| | 2025.4.x  | ✅        | | | 2025.4.x  | ✅        | | ||||||
|  | | 2025.6.x  | ✅        | | ||||||
|  |  | ||||||
| ## Reporting a Vulnerability | ## Reporting a Vulnerability | ||||||
|  |  | ||||||
|  | |||||||
| @ -2,7 +2,7 @@ | |||||||
|  |  | ||||||
| from os import environ | from os import environ | ||||||
|  |  | ||||||
| __version__ = "2025.4.1" | __version__ = "2025.6.3" | ||||||
| ENV_GIT_HASH_KEY = "GIT_BUILD_HASH" | ENV_GIT_HASH_KEY = "GIT_BUILD_HASH" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | |||||||
| @ -148,3 +148,14 @@ class TestBrands(APITestCase): | |||||||
|                 "default_locale": "", |                 "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 F, Q | ||||||
| from django.db.models import Value as V | from django.db.models import Value as V | ||||||
| from django.http.request import HttpRequest | 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 import get_full_version | ||||||
| from authentik.brands.models import Brand | 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""" |     """Context Processor that injects brand object into every template""" | ||||||
|     brand = getattr(request, "brand", DEFAULT_BRAND) |     brand = getattr(request, "brand", DEFAULT_BRAND) | ||||||
|     tenant = getattr(request, "tenant", Tenant()) |     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 { |     return { | ||||||
|         "brand": brand, |         "brand": brand, | ||||||
|  |         "brand_css": brand_css, | ||||||
|         "footer_links": tenant.footer_links, |         "footer_links": tenant.footer_links, | ||||||
|         "html_meta": {**get_http_meta()}, |         "html_meta": {**get_http_meta()}, | ||||||
|         "version": get_full_version(), |         "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.lib.avatars import get_avatar | ||||||
| from authentik.rbac.decorators import permission_required | from authentik.rbac.decorators import permission_required | ||||||
| from authentik.rbac.models import get_permission_choices | 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.models import EmailStage | ||||||
| from authentik.stages.email.tasks import send_mails | from authentik.stages.email.tasks import send_mails | ||||||
| from authentik.stages.email.utils import TemplateEmailMessage | from authentik.stages.email.utils import TemplateEmailMessage | ||||||
| @ -451,7 +452,7 @@ class UserViewSet(UsedByMixin, ModelViewSet): | |||||||
|     def list(self, request, *args, **kwargs): |     def list(self, request, *args, **kwargs): | ||||||
|         return super().list(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), |         """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""" |         that can either be shown to an admin or sent to the user directly""" | ||||||
|         brand: Brand = self.request._request.brand |         brand: Brand = self.request._request.brand | ||||||
| @ -473,12 +474,16 @@ class UserViewSet(UsedByMixin, ModelViewSet): | |||||||
|             raise ValidationError( |             raise ValidationError( | ||||||
|                 {"non_field_errors": "Recovery flow not applicable to user"} |                 {"non_field_errors": "Recovery flow not applicable to user"} | ||||||
|             ) from None |             ) from None | ||||||
|  |         _plan = FlowToken.pickle(plan) | ||||||
|  |         if for_email: | ||||||
|  |             _plan = pickle_flow_token_for_email(plan) | ||||||
|         token, __ = FlowToken.objects.update_or_create( |         token, __ = FlowToken.objects.update_or_create( | ||||||
|             identifier=f"{user.uid}-password-reset", |             identifier=f"{user.uid}-password-reset", | ||||||
|             defaults={ |             defaults={ | ||||||
|                 "user": user, |                 "user": user, | ||||||
|                 "flow": flow, |                 "flow": flow, | ||||||
|                 "_plan": FlowToken.pickle(plan), |                 "_plan": _plan, | ||||||
|  |                 "revoke_on_execution": not for_email, | ||||||
|             }, |             }, | ||||||
|         ) |         ) | ||||||
|         querystring = urlencode({QS_KEY_TOKEN: token.key}) |         querystring = urlencode({QS_KEY_TOKEN: token.key}) | ||||||
| @ -648,7 +653,7 @@ class UserViewSet(UsedByMixin, ModelViewSet): | |||||||
|         if for_user.email == "": |         if for_user.email == "": | ||||||
|             LOGGER.debug("User doesn't have an email address") |             LOGGER.debug("User doesn't have an email address") | ||||||
|             raise ValidationError({"non_field_errors": "User does not have an email address set."}) |             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 |         # Lookup the email stage to assure the current user can access it | ||||||
|         stages = get_objects_for_user( |         stages = get_objects_for_user( | ||||||
|             request.user, "authentik_stages_email.view_emailstage" |             request.user, "authentik_stages_email.view_emailstage" | ||||||
|  | |||||||
| @ -79,6 +79,7 @@ def _migrate_session( | |||||||
|         AuthenticatedSession.objects.using(db_alias).create( |         AuthenticatedSession.objects.using(db_alias).create( | ||||||
|             session=session, |             session=session, | ||||||
|             user=old_auth_session.user, |             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 | # 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.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 | 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( | def remove_old_authenticated_session_content_type( | ||||||
|     apps: Apps, schema_editor: BaseDatabaseSchemaEditor |     apps: Apps, schema_editor: BaseDatabaseSchemaEditor | ||||||
| ): | ): | ||||||
| @ -21,7 +92,12 @@ class Migration(migrations.Migration): | |||||||
|     ] |     ] | ||||||
|  |  | ||||||
|     operations = [ |     operations = [ | ||||||
|  |         migrations.RunPython( | ||||||
|  |             code=migrate_authenticated_session_permissions, | ||||||
|  |             reverse_code=migrations.RunPython.noop, | ||||||
|  |         ), | ||||||
|         migrations.RunPython( |         migrations.RunPython( | ||||||
|             code=remove_old_authenticated_session_content_type, |             code=remove_old_authenticated_session_content_type, | ||||||
|  |             reverse_code=migrations.RunPython.noop, | ||||||
|         ), |         ), | ||||||
|     ] |     ] | ||||||
|  | |||||||
| @ -16,7 +16,7 @@ | |||||||
|         {% block head_before %} |         {% block head_before %} | ||||||
|         {% endblock %} |         {% endblock %} | ||||||
|         <link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}"> |         <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/poly-%v.js' %}" type="module"></script> | ||||||
|         <script src="{% versioned_script 'dist/standalone/loading/index-%v.js' %}" type="module"></script> |         <script src="{% versioned_script 'dist/standalone/loading/index-%v.js' %}" type="module"></script> | ||||||
|         {% block head %} |         {% block head %} | ||||||
|  | |||||||
| @ -15,13 +15,13 @@ class MMDBContextProcessor(EventContextProcessor): | |||||||
|         self.reader: Reader | None = None |         self.reader: Reader | None = None | ||||||
|         self._last_mtime: float = 0.0 |         self._last_mtime: float = 0.0 | ||||||
|         self.logger = get_logger() |         self.logger = get_logger() | ||||||
|         self.open() |         self.load() | ||||||
|  |  | ||||||
|     def path(self) -> str | None: |     def path(self) -> str | None: | ||||||
|         """Get the path to the MMDB file to load""" |         """Get the path to the MMDB file to load""" | ||||||
|         raise NotImplementedError |         raise NotImplementedError | ||||||
|  |  | ||||||
|     def open(self): |     def load(self): | ||||||
|         """Get GeoIP Reader, if configured, otherwise none""" |         """Get GeoIP Reader, if configured, otherwise none""" | ||||||
|         path = self.path() |         path = self.path() | ||||||
|         if path == "" or not path: |         if path == "" or not path: | ||||||
| @ -44,7 +44,7 @@ class MMDBContextProcessor(EventContextProcessor): | |||||||
|             diff = self._last_mtime < mtime |             diff = self._last_mtime < mtime | ||||||
|             if diff > 0: |             if diff > 0: | ||||||
|                 self.logger.info("Found new MMDB Database, reopening", diff=diff, path=path) |                 self.logger.info("Found new MMDB Database, reopening", diff=diff, path=path) | ||||||
|                 self.open() |                 self.load() | ||||||
|         except OSError as exc: |         except OSError as exc: | ||||||
|             self.logger.warning("Failed to check MMDB age", exc=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) |     flow = models.ForeignKey(Flow, on_delete=models.CASCADE) | ||||||
|     _plan = models.TextField() |     _plan = models.TextField() | ||||||
|  |     revoke_on_execution = models.BooleanField(default=True) | ||||||
|  |  | ||||||
|     @staticmethod |     @staticmethod | ||||||
|     def pickle(plan) -> str: |     def pickle(plan: "FlowPlan") -> str: | ||||||
|         """Pickle into string""" |         """Pickle into string""" | ||||||
|         data = dumps(plan) |         data = dumps(plan) | ||||||
|         return b64encode(data).decode() |         return b64encode(data).decode() | ||||||
|  | |||||||
| @ -99,9 +99,10 @@ class ChallengeStageView(StageView): | |||||||
|             self.logger.debug("Got StageInvalidException", exc=exc) |             self.logger.debug("Got StageInvalidException", exc=exc) | ||||||
|             return self.executor.stage_invalid() |             return self.executor.stage_invalid() | ||||||
|         if not challenge.is_valid(): |         if not challenge.is_valid(): | ||||||
|             self.logger.warning( |             self.logger.error( | ||||||
|                 "f(ch): Invalid challenge", |                 "f(ch): Invalid challenge", | ||||||
|                 errors=challenge.errors, |                 errors=challenge.errors, | ||||||
|  |                 challenge=challenge.data, | ||||||
|             ) |             ) | ||||||
|         return HttpChallengeResponse(challenge) |         return HttpChallengeResponse(challenge) | ||||||
|  |  | ||||||
|  | |||||||
| @ -146,6 +146,7 @@ class FlowExecutorView(APIView): | |||||||
|         except (AttributeError, EOFError, ImportError, IndexError) as exc: |         except (AttributeError, EOFError, ImportError, IndexError) as exc: | ||||||
|             LOGGER.warning("f(exec): Failed to restore token plan", exc=exc) |             LOGGER.warning("f(exec): Failed to restore token plan", exc=exc) | ||||||
|         finally: |         finally: | ||||||
|  |             if token.revoke_on_execution: | ||||||
|                 token.delete() |                 token.delete() | ||||||
|         if not isinstance(plan, FlowPlan): |         if not isinstance(plan, FlowPlan): | ||||||
|             return None |             return None | ||||||
|  | |||||||
| @ -81,7 +81,6 @@ debugger: false | |||||||
|  |  | ||||||
| log_level: info | log_level: info | ||||||
|  |  | ||||||
| session_storage: cache |  | ||||||
| sessions: | sessions: | ||||||
|   unauthenticated_age: days=1 |   unauthenticated_age: days=1 | ||||||
|  |  | ||||||
|  | |||||||
| @ -1,6 +1,7 @@ | |||||||
| from collections.abc import Callable | from collections.abc import Callable | ||||||
| from dataclasses import asdict | from dataclasses import asdict | ||||||
|  |  | ||||||
|  | from celery import group | ||||||
| from celery.exceptions import Retry | from celery.exceptions import Retry | ||||||
| from celery.result import allow_join_result | from celery.result import allow_join_result | ||||||
| from django.core.paginator import Paginator | from django.core.paginator import Paginator | ||||||
| @ -82,21 +83,41 @@ class SyncTasks: | |||||||
|                 self.logger.debug("Failed to acquire sync lock, skipping", provider=provider.name) |                 self.logger.debug("Failed to acquire sync lock, skipping", provider=provider.name) | ||||||
|                 return |                 return | ||||||
|             try: |             try: | ||||||
|                 for page in users_paginator.page_range: |                 messages.append(_("Syncing users")) | ||||||
|                     messages.append(_("Syncing page {page} of users".format(page=page))) |                 user_results = ( | ||||||
|                     for msg in sync_objects.apply_async( |                     group( | ||||||
|  |                         [ | ||||||
|  |                             sync_objects.signature( | ||||||
|                                 args=(class_to_path(User), page, provider_pk), |                                 args=(class_to_path(User), page, provider_pk), | ||||||
|                                 time_limit=PAGE_TIMEOUT, |                                 time_limit=PAGE_TIMEOUT, | ||||||
|                                 soft_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)) |                         messages.append(LogEvent(**msg)) | ||||||
|                 for page in groups_paginator.page_range: |                 messages.append(_("Syncing groups")) | ||||||
|                     messages.append(_("Syncing page {page} of groups".format(page=page))) |                 group_results = ( | ||||||
|                     for msg in sync_objects.apply_async( |                     group( | ||||||
|  |                         [ | ||||||
|  |                             sync_objects.signature( | ||||||
|                                 args=(class_to_path(Group), page, provider_pk), |                                 args=(class_to_path(Group), page, provider_pk), | ||||||
|                                 time_limit=PAGE_TIMEOUT, |                                 time_limit=PAGE_TIMEOUT, | ||||||
|                                 soft_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)) |                         messages.append(LogEvent(**msg)) | ||||||
|             except TransientSyncException as exc: |             except TransientSyncException as exc: | ||||||
|                 self.logger.warning("transient sync exception", exc=exc) |                 self.logger.warning("transient sync exception", exc=exc) | ||||||
| @ -109,7 +130,7 @@ class SyncTasks: | |||||||
|     def sync_objects( |     def sync_objects( | ||||||
|         self, object_type: str, page: int, provider_pk: int, override_dry_run=False, **filter |         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( |         self.logger = get_logger().bind( | ||||||
|             provider_type=class_to_path(self._provider_model), |             provider_type=class_to_path(self._provider_model), | ||||||
|             provider_pk=provider_pk, |             provider_pk=provider_pk, | ||||||
| @ -132,6 +153,19 @@ class SyncTasks: | |||||||
|             self.logger.debug("starting discover") |             self.logger.debug("starting discover") | ||||||
|             client.discover() |             client.discover() | ||||||
|         self.logger.debug("starting sync for page", page=page) |         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: |         for obj in paginator.page(page).object_list: | ||||||
|             obj: Model |             obj: Model | ||||||
|             try: |             try: | ||||||
|  | |||||||
| @ -1,11 +1,9 @@ | |||||||
| """Websocket tests""" | """Websocket tests""" | ||||||
|  |  | ||||||
| from dataclasses import asdict | from dataclasses import asdict | ||||||
| from unittest.mock import patch |  | ||||||
|  |  | ||||||
| from channels.routing import URLRouter | from channels.routing import URLRouter | ||||||
| from channels.testing import WebsocketCommunicator | from channels.testing import WebsocketCommunicator | ||||||
| from django.contrib.contenttypes.models import ContentType |  | ||||||
| from django.test import TransactionTestCase | from django.test import TransactionTestCase | ||||||
|  |  | ||||||
| from authentik import __version__ | from authentik import __version__ | ||||||
| @ -16,12 +14,6 @@ from authentik.providers.proxy.models import ProxyProvider | |||||||
| from authentik.root import websocket | 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): | class TestOutpostWS(TransactionTestCase): | ||||||
|     """Websocket tests""" |     """Websocket tests""" | ||||||
|  |  | ||||||
|  | |||||||
| @ -66,7 +66,10 @@ class RACClientConsumer(AsyncWebsocketConsumer): | |||||||
|     def init_outpost_connection(self): |     def init_outpost_connection(self): | ||||||
|         """Initialize guac connection settings""" |         """Initialize guac connection settings""" | ||||||
|         self.token = ( |         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") |             .select_related("endpoint", "provider", "session", "session__user") | ||||||
|             .first() |             .first() | ||||||
|         ) |         ) | ||||||
|  | |||||||
| @ -166,7 +166,6 @@ class ConnectionToken(ExpiringModel): | |||||||
|         always_merger.merge(settings, default_settings) |         always_merger.merge(settings, default_settings) | ||||||
|         always_merger.merge(settings, self.endpoint.provider.settings) |         always_merger.merge(settings, self.endpoint.provider.settings) | ||||||
|         always_merger.merge(settings, self.endpoint.settings) |         always_merger.merge(settings, self.endpoint.settings) | ||||||
|         always_merger.merge(settings, self.settings) |  | ||||||
|  |  | ||||||
|         def mapping_evaluator(mappings: QuerySet): |         def mapping_evaluator(mappings: QuerySet): | ||||||
|             for mapping in mappings: |             for mapping in mappings: | ||||||
| @ -191,6 +190,7 @@ class ConnectionToken(ExpiringModel): | |||||||
|         mapping_evaluator( |         mapping_evaluator( | ||||||
|             RACPropertyMapping.objects.filter(endpoint__in=[self.endpoint]).order_by("name") |             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["drive-path"] = f"/tmp/connection/{self.token}"  # nosec | ||||||
|         settings["create-drive-path"] = "true" |         settings["create-drive-path"] = "true" | ||||||
|  | |||||||
| @ -90,23 +90,6 @@ class TestModels(TransactionTestCase): | |||||||
|                 "resize-method": "display-update", |                 "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) |         # Set settings in property mapping (provider) | ||||||
|         mapping = RACPropertyMapping.objects.create( |         mapping = RACPropertyMapping.objects.create( | ||||||
|             name=generate_id(), |             name=generate_id(), | ||||||
| @ -151,3 +134,22 @@ class TestModels(TransactionTestCase): | |||||||
|                 "resize-method": "display-update", |                 "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) |         body = loads(flow_response.content) | ||||||
|         self.assertEqual(body["component"], "ak-stage-access-denied") |         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.engine import PolicyEngine | ||||||
| from authentik.policies.views import PolicyAccessView | from authentik.policies.views import PolicyAccessView | ||||||
| from authentik.providers.rac.models import ConnectionToken, Endpoint, RACProvider | 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): | class RACStartView(PolicyAccessView): | ||||||
| @ -65,7 +68,10 @@ class RACInterface(InterfaceView): | |||||||
|  |  | ||||||
|     def dispatch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse: |     def dispatch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse: | ||||||
|         # Early sanity check to ensure token still exists |         # 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: |         if not token: | ||||||
|             return redirect("authentik_core:if-user") |             return redirect("authentik_core:if-user") | ||||||
|         self.token = token |         self.token = token | ||||||
| @ -109,10 +115,15 @@ class RACFinalStage(RedirectStage): | |||||||
|         return super().dispatch(request, *args, **kwargs) |         return super().dispatch(request, *args, **kwargs) | ||||||
|  |  | ||||||
|     def get_challenge(self, *args, **kwargs) -> RedirectChallenge: |     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( |         token = ConnectionToken.objects.create( | ||||||
|             provider=self.provider, |             provider=self.provider, | ||||||
|             endpoint=self.endpoint, |             endpoint=self.endpoint, | ||||||
|             settings=self.executor.plan.context.get("connection_settings", {}), |             settings=settings or {}, | ||||||
|             session=self.request.session["authenticatedsession"], |             session=self.request.session["authenticatedsession"], | ||||||
|             expires=now() + timedelta_from_string(self.provider.connection_expiry), |             expires=now() + timedelta_from_string(self.provider.connection_expiry), | ||||||
|             expiring=True, |             expiring=True, | ||||||
|  | |||||||
| @ -47,15 +47,16 @@ class SCIMGroupClient(SCIMClient[Group, SCIMProviderGroup, SCIMGroupSchema]): | |||||||
|  |  | ||||||
|     def to_schema(self, obj: Group, connection: SCIMProviderGroup) -> SCIMGroupSchema: |     def to_schema(self, obj: Group, connection: SCIMProviderGroup) -> SCIMGroupSchema: | ||||||
|         """Convert authentik user into SCIM""" |         """Convert authentik user into SCIM""" | ||||||
|         raw_scim_group = super().to_schema( |         raw_scim_group = super().to_schema(obj, connection) | ||||||
|             obj, |  | ||||||
|             connection, |  | ||||||
|             schemas=(SCIM_GROUP_SCHEMA,), |  | ||||||
|         ) |  | ||||||
|         try: |         try: | ||||||
|             scim_group = SCIMGroupSchema.model_validate(delete_none_values(raw_scim_group)) |             scim_group = SCIMGroupSchema.model_validate(delete_none_values(raw_scim_group)) | ||||||
|         except ValidationError as exc: |         except ValidationError as exc: | ||||||
|             raise StopSync(exc, obj) from 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: |         if not scim_group.externalId: | ||||||
|             scim_group.externalId = str(obj.pk) |             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: |     def to_schema(self, obj: User, connection: SCIMProviderUser) -> SCIMUserSchema: | ||||||
|         """Convert authentik user into SCIM""" |         """Convert authentik user into SCIM""" | ||||||
|         raw_scim_user = super().to_schema( |         raw_scim_user = super().to_schema(obj, connection) | ||||||
|             obj, |  | ||||||
|             connection, |  | ||||||
|             schemas=(SCIM_USER_SCHEMA,), |  | ||||||
|         ) |  | ||||||
|         try: |         try: | ||||||
|             scim_user = SCIMUserSchema.model_validate(delete_none_values(raw_scim_user)) |             scim_user = SCIMUserSchema.model_validate(delete_none_values(raw_scim_user)) | ||||||
|         except ValidationError as exc: |         except ValidationError as exc: | ||||||
|             raise StopSync(exc, obj) from 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: |         if not scim_user.externalId: | ||||||
|             scim_user.externalId = str(obj.uid) |             scim_user.externalId = str(obj.uid) | ||||||
|         return scim_user |         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() |     @Mocker() | ||||||
|     def test_user_create_different_provider_same_id(self, mock: Mocker): |     def test_user_create_different_provider_same_id(self, mock: Mocker): | ||||||
|         """Test user creation with multiple providers that happen |         """Test user creation with multiple providers that happen | ||||||
| @ -384,7 +435,7 @@ class SCIMUserTests(TestCase): | |||||||
|                 self.assertIn(request.method, SAFE_METHODS) |                 self.assertIn(request.method, SAFE_METHODS) | ||||||
|         task = SystemTask.objects.filter(uid=slugify(self.provider.name)).first() |         task = SystemTask.objects.filter(uid=slugify(self.provider.name)).first() | ||||||
|         self.assertIsNotNone(task) |         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.assertEqual(drop_msg["event"], "Dropping mutating request due to dry run") | ||||||
|         self.assertIsNotNone(drop_msg["attributes"]["url"]) |         self.assertIsNotNone(drop_msg["attributes"]["url"]) | ||||||
|         self.assertIsNotNone(drop_msg["attributes"]["body"]) |         self.assertIsNotNone(drop_msg["attributes"]["body"]) | ||||||
|  | |||||||
| @ -424,7 +424,7 @@ else: | |||||||
|         "BACKEND": "authentik.root.storages.FileStorage", |         "BACKEND": "authentik.root.storages.FileStorage", | ||||||
|         "OPTIONS": { |         "OPTIONS": { | ||||||
|             "location": Path(CONFIG.get("storage.media.file.path")), |             "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 |     # Compatibility for apps not supporting top-level STORAGES | ||||||
|  | |||||||
| @ -3,25 +3,46 @@ | |||||||
| import os | import os | ||||||
| from argparse import ArgumentParser | from argparse import ArgumentParser | ||||||
| from unittest import TestCase | from unittest import TestCase | ||||||
|  | from unittest.mock import patch | ||||||
|  |  | ||||||
| import pytest | import pytest | ||||||
| from django.conf import settings | from django.conf import settings | ||||||
|  | from django.contrib.contenttypes.models import ContentType | ||||||
| from django.test.runner import DiscoverRunner | 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.config import CONFIG | ||||||
| from authentik.lib.sentry import sentry_init | from authentik.lib.sentry import sentry_init | ||||||
| from authentik.root.signals import post_startup, pre_startup, startup | 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 | # globally set maxDiff to none to show full assert error | ||||||
| TestCase.maxDiff = None | 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 | class PytestTestRunner(DiscoverRunner):  # pragma: no cover | ||||||
|     """Runs pytest to discover and run tests.""" |     """Runs pytest to discover and run tests.""" | ||||||
|  |  | ||||||
|     def __init__(self, **kwargs): |     def __init__(self, **kwargs): | ||||||
|         super().__init__(**kwargs) |         super().__init__(**kwargs) | ||||||
|  |         self.logger = get_logger().bind(runner="pytest") | ||||||
|  |  | ||||||
|         self.args = [] |         self.args = [] | ||||||
|         if self.failfast: |         if self.failfast: | ||||||
| @ -31,6 +52,8 @@ class PytestTestRunner(DiscoverRunner):  # pragma: no cover | |||||||
|  |  | ||||||
|         if kwargs.get("randomly_seed", None): |         if kwargs.get("randomly_seed", None): | ||||||
|             self.args.append(f"--randomly-seed={kwargs['randomly_seed']}") |             self.args.append(f"--randomly-seed={kwargs['randomly_seed']}") | ||||||
|  |         if kwargs.get("no_capture", False): | ||||||
|  |             self.args.append("--capture=no") | ||||||
|  |  | ||||||
|         settings.TEST = True |         settings.TEST = True | ||||||
|         settings.CELERY["task_always_eager"] = 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.sample_rate", 0) | ||||||
|         CONFIG.set("error_reporting.environment", "testing") |         CONFIG.set("error_reporting.environment", "testing") | ||||||
|         CONFIG.set("error_reporting.send_pii", True) |         CONFIG.set("error_reporting.send_pii", True) | ||||||
|  |  | ||||||
|  |         ASN_CONTEXT_PROCESSOR.load() | ||||||
|  |         GEOIP_CONTEXT_PROCESSOR.load() | ||||||
|  |  | ||||||
|         sentry_init() |         sentry_init() | ||||||
|  |  | ||||||
|         pre_startup.send(sender=self, mode="test") |         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" |             "Default behaviour: use random.Random().getrandbits(32), so the seed is" | ||||||
|             "different on each run.", |             "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): |     def run_tests(self, test_labels, extra_tests=None, **kwargs): | ||||||
|         """Run pytest and return the exitcode. |         """Run pytest and return the exitcode. | ||||||
| @ -106,4 +138,10 @@ class PytestTestRunner(DiscoverRunner):  # pragma: no cover | |||||||
|                     f"path instead." |                     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) |                 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", |             "user_object_filter", | ||||||
|             "group_object_filter", |             "group_object_filter", | ||||||
|             "group_membership_field", |             "group_membership_field", | ||||||
|  |             "user_membership_attribute", | ||||||
|             "object_uniqueness_field", |             "object_uniqueness_field", | ||||||
|             "password_login_update_internal_password", |             "password_login_update_internal_password", | ||||||
|             "sync_users", |             "sync_users", | ||||||
| @ -111,6 +112,7 @@ class LDAPSourceSerializer(SourceSerializer): | |||||||
|             "sync_parent_group", |             "sync_parent_group", | ||||||
|             "connectivity", |             "connectivity", | ||||||
|             "lookup_groups_from_user", |             "lookup_groups_from_user", | ||||||
|  |             "delete_not_found_objects", | ||||||
|         ] |         ] | ||||||
|         extra_kwargs = {"bind_password": {"write_only": True}} |         extra_kwargs = {"bind_password": {"write_only": True}} | ||||||
|  |  | ||||||
| @ -138,6 +140,7 @@ class LDAPSourceViewSet(UsedByMixin, ModelViewSet): | |||||||
|         "user_object_filter", |         "user_object_filter", | ||||||
|         "group_object_filter", |         "group_object_filter", | ||||||
|         "group_membership_field", |         "group_membership_field", | ||||||
|  |         "user_membership_attribute", | ||||||
|         "object_uniqueness_field", |         "object_uniqueness_field", | ||||||
|         "password_login_update_internal_password", |         "password_login_update_internal_password", | ||||||
|         "sync_users", |         "sync_users", | ||||||
| @ -147,6 +150,7 @@ class LDAPSourceViewSet(UsedByMixin, ModelViewSet): | |||||||
|         "user_property_mappings", |         "user_property_mappings", | ||||||
|         "group_property_mappings", |         "group_property_mappings", | ||||||
|         "lookup_groups_from_user", |         "lookup_groups_from_user", | ||||||
|  |         "delete_not_found_objects", | ||||||
|     ] |     ] | ||||||
|     search_fields = ["name", "slug"] |     search_fields = ["name", "slug"] | ||||||
|     ordering = ["name"] |     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)", |         default="(objectClass=person)", | ||||||
|         help_text=_("Consider Objects matching this filter to be Users."), |         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( |     group_membership_field = models.TextField( | ||||||
|         default="member", help_text=_("Field which contains members of a group.") |         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 |     @property | ||||||
|     def component(self) -> str: |     def component(self) -> str: | ||||||
|         return "ak-source-ldap-form" |         return "ak-source-ldap-form" | ||||||
| @ -321,6 +333,12 @@ class LDAPSourcePropertyMapping(PropertyMapping): | |||||||
|  |  | ||||||
|  |  | ||||||
| class UserLDAPSourceConnection(UserSourceConnection): | 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 |     @property | ||||||
|     def serializer(self) -> type[Serializer]: |     def serializer(self) -> type[Serializer]: | ||||||
|         from authentik.sources.ldap.api import ( |         from authentik.sources.ldap.api import ( | ||||||
| @ -332,9 +350,18 @@ class UserLDAPSourceConnection(UserSourceConnection): | |||||||
|     class Meta: |     class Meta: | ||||||
|         verbose_name = _("User LDAP Source Connection") |         verbose_name = _("User LDAP Source Connection") | ||||||
|         verbose_name_plural = _("User LDAP Source Connections") |         verbose_name_plural = _("User LDAP Source Connections") | ||||||
|  |         indexes = [ | ||||||
|  |             models.Index(fields=["validated_by"]), | ||||||
|  |         ] | ||||||
|  |  | ||||||
|  |  | ||||||
| class GroupLDAPSourceConnection(GroupSourceConnection): | 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 |     @property | ||||||
|     def serializer(self) -> type[Serializer]: |     def serializer(self) -> type[Serializer]: | ||||||
|         from authentik.sources.ldap.api import ( |         from authentik.sources.ldap.api import ( | ||||||
| @ -346,3 +373,6 @@ class GroupLDAPSourceConnection(GroupSourceConnection): | |||||||
|     class Meta: |     class Meta: | ||||||
|         verbose_name = _("Group LDAP Source Connection") |         verbose_name = _("Group LDAP Source Connection") | ||||||
|         verbose_name_plural = _("Group LDAP Source Connections") |         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.core.sources.mapper import SourceMapper | ||||||
| from authentik.lib.config import CONFIG | from authentik.lib.config import CONFIG | ||||||
| from authentik.lib.sync.mapper import PropertyMappingManager | from authentik.lib.sync.mapper import PropertyMappingManager | ||||||
| from authentik.sources.ldap.models import LDAPSource | from authentik.sources.ldap.models import LDAPSource, flatten | ||||||
|  |  | ||||||
|  |  | ||||||
| class BaseLDAPSynchronizer: | class BaseLDAPSynchronizer: | ||||||
| @ -77,6 +77,16 @@ class BaseLDAPSynchronizer: | |||||||
|         """Get objects from LDAP, implemented in subclass""" |         """Get objects from LDAP, implemented in subclass""" | ||||||
|         raise NotImplementedError() |         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 |     def search_paginator(  # noqa: PLR0913 | ||||||
|         self, |         self, | ||||||
|         search_base, |         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 |             return -1 | ||||||
|         group_count = 0 |         group_count = 0 | ||||||
|         for group in page_data: |         for group in page_data: | ||||||
|             if "attributes" not in group: |             if (attributes := self.get_attributes(group)) is None: | ||||||
|                 continue |                 continue | ||||||
|             attributes = group.get("attributes", {}) |  | ||||||
|             group_dn = flatten(flatten(group.get("entryDN", group.get("dn")))) |             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( |                 self.message( | ||||||
|                     f"Uniqueness field not found/not set in attributes: '{group_dn}'", |                     f"Uniqueness field not found/not set in attributes: '{group_dn}'", | ||||||
|                     attributes=attributes.keys(), |                     attributes=attributes.keys(), | ||||||
|                     dn=group_dn, |                     dn=group_dn, | ||||||
|                 ) |                 ) | ||||||
|                 continue |                 continue | ||||||
|             uniq = flatten(attributes[self._source.object_uniqueness_field]) |  | ||||||
|             try: |             try: | ||||||
|                 defaults = { |                 defaults = { | ||||||
|                     k: flatten(v) |                     k: flatten(v) | ||||||
|  | |||||||
| @ -63,25 +63,19 @@ class MembershipLDAPSynchronizer(BaseLDAPSynchronizer): | |||||||
|                     group_member_dn = group_member.get("dn", {}) |                     group_member_dn = group_member.get("dn", {}) | ||||||
|                     members.append(group_member_dn) |                     members.append(group_member_dn) | ||||||
|             else: |             else: | ||||||
|                 if "attributes" not in group: |                 if (attributes := self.get_attributes(group)) is None: | ||||||
|                     continue |                     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) |             ak_group = self.get_group(group) | ||||||
|             if not ak_group: |             if not ak_group: | ||||||
|                 continue |                 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( |             users = User.objects.filter( | ||||||
|                 Q(**{f"attributes__{membership_mapping_attribute}__in": members}) |                 Q(**{f"attributes__{self._source.user_membership_attribute}__in": members}) | ||||||
|                 | Q( |                 | Q( | ||||||
|                     **{ |                     **{ | ||||||
|                         f"attributes__{membership_mapping_attribute}__isnull": True, |                         f"attributes__{self._source.user_membership_attribute}__isnull": True, | ||||||
|                         "ak_groups__in": [ak_group], |                         "ak_groups__in": [ak_group], | ||||||
|                     } |                     } | ||||||
|                 ) |                 ) | ||||||
|  | |||||||
| @ -60,18 +60,16 @@ class UserLDAPSynchronizer(BaseLDAPSynchronizer): | |||||||
|             return -1 |             return -1 | ||||||
|         user_count = 0 |         user_count = 0 | ||||||
|         for user in page_data: |         for user in page_data: | ||||||
|             if "attributes" not in user: |             if (attributes := self.get_attributes(user)) is None: | ||||||
|                 continue |                 continue | ||||||
|             attributes = user.get("attributes", {}) |  | ||||||
|             user_dn = flatten(user.get("entryDN", user.get("dn"))) |             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( |                 self.message( | ||||||
|                     f"Uniqueness field not found/not set in attributes: '{user_dn}'", |                     f"Uniqueness field not found/not set in attributes: '{user_dn}'", | ||||||
|                     attributes=attributes.keys(), |                     attributes=attributes.keys(), | ||||||
|                     dn=user_dn, |                     dn=user_dn, | ||||||
|                 ) |                 ) | ||||||
|                 continue |                 continue | ||||||
|             uniq = flatten(attributes[self._source.object_uniqueness_field]) |  | ||||||
|             try: |             try: | ||||||
|                 defaults = { |                 defaults = { | ||||||
|                     k: flatten(v) |                     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.root.celery import CELERY_APP | ||||||
| from authentik.sources.ldap.models import LDAPSource | from authentik.sources.ldap.models import LDAPSource | ||||||
| from authentik.sources.ldap.sync.base import BaseLDAPSynchronizer | 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.groups import GroupLDAPSynchronizer | ||||||
| from authentik.sources.ldap.sync.membership import MembershipLDAPSynchronizer | from authentik.sources.ldap.sync.membership import MembershipLDAPSynchronizer | ||||||
| from authentik.sources.ldap.sync.users import UserLDAPSynchronizer | from authentik.sources.ldap.sync.users import UserLDAPSynchronizer | ||||||
| @ -52,11 +54,11 @@ def ldap_connectivity_check(pk: str | None = None): | |||||||
|  |  | ||||||
|  |  | ||||||
| @CELERY_APP.task( | @CELERY_APP.task( | ||||||
|     # We take the configured hours timeout time by 2.5 as we run user and |     # We take the configured hours timeout time by 3.5 as we run user and | ||||||
|     # group in parallel and then membership, so 2x is to cover the serial tasks, |     # 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 |     # 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, |     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")) * 2.5, |     task_time_limit=(60 * 60 * CONFIG.get_int("ldap.task_timeout_hours")) * 3.5, | ||||||
| ) | ) | ||||||
| def ldap_sync_single(source_pk: str): | def ldap_sync_single(source_pk: str): | ||||||
|     """Sync a single source""" |     """Sync a single source""" | ||||||
| @ -69,18 +71,31 @@ def ldap_sync_single(source_pk: str): | |||||||
|             return |             return | ||||||
|         # Delete all sync tasks from the cache |         # Delete all sync tasks from the cache | ||||||
|         DBSystemTask.objects.filter(name="ldap_sync", uid__startswith=source.slug).delete() |         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 |         # The order of these operations needs to be preserved as each depends on the previous one(s) | ||||||
|             group( |         # 1. User and group sync can happen simultaneously | ||||||
|                 ldap_sync_paginator(source, UserLDAPSynchronizer) |         # 2. Membership sync needs to run afterwards | ||||||
|                 + ldap_sync_paginator(source, GroupLDAPSynchronizer), |         # 3. Finally, user and group deletions can happen simultaneously | ||||||
|             ), |         user_group_sync = ldap_sync_paginator(source, UserLDAPSynchronizer) + ldap_sync_paginator( | ||||||
|             # Membership sync needs to run afterwards |             source, GroupLDAPSynchronizer | ||||||
|             group( |  | ||||||
|                 ldap_sync_paginator(source, MembershipLDAPSynchronizer), |  | ||||||
|             ), |  | ||||||
|         ) |         ) | ||||||
|         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: | 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 | 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: | def mock_slapd_connection(password: str) -> Connection: | ||||||
|     """Create mock SLAPD connection""" |     """Create mock SLAPD connection""" | ||||||
| @ -96,5 +123,14 @@ def mock_slapd_connection(password: str) -> Connection: | |||||||
|             "objectClass": "posixAccount", |             "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() |     connection.bind() | ||||||
|     return connection |     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.generators import generate_id, generate_key | ||||||
| from authentik.lib.sync.outgoing.exceptions import StopSync | from authentik.lib.sync.outgoing.exceptions import StopSync | ||||||
| from authentik.lib.utils.reflection import class_to_path | 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.groups import GroupLDAPSynchronizer | ||||||
| from authentik.sources.ldap.sync.membership import MembershipLDAPSynchronizer | from authentik.sources.ldap.sync.membership import MembershipLDAPSynchronizer | ||||||
| from authentik.sources.ldap.sync.users import UserLDAPSynchronizer | from authentik.sources.ldap.sync.users import UserLDAPSynchronizer | ||||||
| from authentik.sources.ldap.tasks import ldap_sync, ldap_sync_all | 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_ad import mock_ad_connection | ||||||
| from authentik.sources.ldap.tests.mock_freeipa import mock_freeipa_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() | LDAP_PASSWORD = generate_key() | ||||||
|  |  | ||||||
| @ -257,11 +269,55 @@ class LDAPSyncTests(TestCase): | |||||||
|         self.source.group_membership_field = "memberUid" |         self.source.group_membership_field = "memberUid" | ||||||
|         self.source.user_object_filter = "(objectClass=posixAccount)" |         self.source.user_object_filter = "(objectClass=posixAccount)" | ||||||
|         self.source.group_object_filter = "(objectClass=posixGroup)" |         self.source.group_object_filter = "(objectClass=posixGroup)" | ||||||
|  |         self.source.user_membership_attribute = "uid" | ||||||
|         self.source.user_property_mappings.set( |         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/default") | ||||||
|                     | Q(managed__startswith="goauthentik.io/sources/ldap/openldap") |                     | 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( |         self.source.group_property_mappings.set( | ||||||
|             LDAPSourcePropertyMapping.objects.filter( |             LDAPSourcePropertyMapping.objects.filter( | ||||||
| @ -308,3 +364,160 @@ class LDAPSyncTests(TestCase): | |||||||
|         connection = MagicMock(return_value=mock_slapd_connection(LDAP_PASSWORD)) |         connection = MagicMock(return_value=mock_slapd_connection(LDAP_PASSWORD)) | ||||||
|         with patch("authentik.sources.ldap.models.LDAPSource.connection", connection): |         with patch("authentik.sources.ldap.models.LDAPSource.connection", connection): | ||||||
|             ldap_sync_all.delay().get() |             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.shortcuts import get_object_or_404, redirect | ||||||
| from django.utils.decorators import method_decorator | from django.utils.decorators import method_decorator | ||||||
| from django.utils.http import urlencode | from django.utils.http import urlencode | ||||||
|  | from django.utils.translation import gettext as _ | ||||||
| from django.views import View | from django.views import View | ||||||
| from django.views.decorators.csrf import csrf_exempt | from django.views.decorators.csrf import csrf_exempt | ||||||
| from structlog.stdlib import get_logger | from structlog.stdlib import get_logger | ||||||
| @ -128,7 +129,9 @@ class InitiateView(View): | |||||||
|         # otherwise we default to POST_AUTO, with direct redirect |         # otherwise we default to POST_AUTO, with direct redirect | ||||||
|         if source.binding_type == SAMLBindingTypes.POST: |         if source.binding_type == SAMLBindingTypes.POST: | ||||||
|             injected_stages.append(in_memory_stage(ConsentStageView)) |             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)) |         injected_stages.append(in_memory_stage(AutosubmitStageView)) | ||||||
|         return self.handle_login_flow( |         return self.handle_login_flow( | ||||||
|             source, |             source, | ||||||
|  | |||||||
| @ -151,9 +151,7 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase): | |||||||
|             webauthn_user_verification=UserVerification.PREFERRED, |             webauthn_user_verification=UserVerification.PREFERRED, | ||||||
|         ) |         ) | ||||||
|         stage.webauthn_allowed_device_types.set( |         stage.webauthn_allowed_device_types.set( | ||||||
|             WebAuthnDeviceType.objects.filter( |             WebAuthnDeviceType.objects.filter(description="YubiKey 5 Series") | ||||||
|                 description="Android Authenticator with SafetyNet Attestation" |  | ||||||
|             ) |  | ||||||
|         ) |         ) | ||||||
|         session = self.client.session |         session = self.client.session | ||||||
|         plan = FlowPlan(flow_pk=flow.pk.hex) |         plan = FlowPlan(flow_pk=flow.pk.hex) | ||||||
| @ -339,9 +337,7 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase): | |||||||
|             device_classes=[DeviceClasses.WEBAUTHN], |             device_classes=[DeviceClasses.WEBAUTHN], | ||||||
|         ) |         ) | ||||||
|         stage.webauthn_allowed_device_types.set( |         stage.webauthn_allowed_device_types.set( | ||||||
|             WebAuthnDeviceType.objects.filter( |             WebAuthnDeviceType.objects.filter(description="YubiKey 5 Series") | ||||||
|                 description="Android Authenticator with SafetyNet Attestation" |  | ||||||
|             ) |  | ||||||
|         ) |         ) | ||||||
|         session = self.client.session |         session = self.client.session | ||||||
|         plan = FlowPlan(flow_pk=flow.pk.hex) |         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)""" |         """Test registration with restricted devices (fail)""" | ||||||
|         webauthn_mds_import.delay(force=True).get() |         webauthn_mds_import.delay(force=True).get() | ||||||
|         self.stage.device_type_restrictions.set( |         self.stage.device_type_restrictions.set( | ||||||
|             WebAuthnDeviceType.objects.filter( |             WebAuthnDeviceType.objects.filter(description="YubiKey 5 Series") | ||||||
|                 description="Android Authenticator with SafetyNet Attestation" |  | ||||||
|             ) |  | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|         plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]) |         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.http import HttpRequest, HttpResponse | ||||||
| from django.utils.timezone import now | 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 rest_framework.fields import CharField | ||||||
|  |  | ||||||
| from authentik.core.api.utils import PassiveSerializer | from authentik.core.api.utils import PassiveSerializer | ||||||
| @ -47,6 +49,11 @@ class ConsentChallengeResponse(ChallengeResponse): | |||||||
|     component = CharField(default="ak-stage-consent") |     component = CharField(default="ak-stage-consent") | ||||||
|     token = CharField(required=True) |     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): | class ConsentStageView(ChallengeStageView): | ||||||
|     """Simple consent checker.""" |     """Simple consent checker.""" | ||||||
| @ -120,9 +127,6 @@ class ConsentStageView(ChallengeStageView): | |||||||
|         return super().get(request, *args, **kwargs) |         return super().get(request, *args, **kwargs) | ||||||
|  |  | ||||||
|     def challenge_valid(self, response: ChallengeResponse) -> HttpResponse: |     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(): |         if self.should_always_prompt(): | ||||||
|             return self.executor.stage_ok() |             return self.executor.stage_ok() | ||||||
|         current_stage: ConsentStage = self.executor.current_stage |         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.lib.generators import generate_id | ||||||
| from authentik.stages.consent.models import ConsentMode, ConsentStage, UserConsent | from authentik.stages.consent.models import ConsentMode, ConsentStage, UserConsent | ||||||
| from authentik.stages.consent.stage import ( | from authentik.stages.consent.stage import ( | ||||||
|  |     PLAN_CONTEXT_CONSENT_HEADER, | ||||||
|     PLAN_CONTEXT_CONSENT_PERMISSIONS, |     PLAN_CONTEXT_CONSENT_PERMISSIONS, | ||||||
|     SESSION_KEY_CONSENT_TOKEN, |     SESSION_KEY_CONSENT_TOKEN, | ||||||
| ) | ) | ||||||
| @ -33,6 +34,40 @@ class TestConsentStage(FlowTestCase): | |||||||
|             slug=generate_id(), |             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): |     def test_always_required(self): | ||||||
|         """Test always required consent""" |         """Test always required consent""" | ||||||
|         flow = create_test_flow(FlowDesignation.AUTHENTICATION) |         flow = create_test_flow(FlowDesignation.AUTHENTICATION) | ||||||
| @ -158,6 +193,7 @@ class TestConsentStage(FlowTestCase): | |||||||
|             context={ |             context={ | ||||||
|                 PLAN_CONTEXT_APPLICATION: self.application, |                 PLAN_CONTEXT_APPLICATION: self.application, | ||||||
|                 PLAN_CONTEXT_CONSENT_PERMISSIONS: [PermissionDict(id="foo", name="foo-desc")], |                 PLAN_CONTEXT_CONSENT_PERMISSIONS: [PermissionDict(id="foo", name="foo-desc")], | ||||||
|  |                 PLAN_CONTEXT_CONSENT_HEADER: "test header", | ||||||
|             }, |             }, | ||||||
|         ) |         ) | ||||||
|         session = self.client.session |         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.flows.views.executor import QS_KEY_TOKEN, QS_QUERY | ||||||
| from authentik.lib.utils.errors import exception_to_string | from authentik.lib.utils.errors import exception_to_string | ||||||
| from authentik.lib.utils.time import timedelta_from_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.models import EmailStage | ||||||
| from authentik.stages.email.tasks import send_mails | from authentik.stages.email.tasks import send_mails | ||||||
| from authentik.stages.email.utils import TemplateEmailMessage | from authentik.stages.email.utils import TemplateEmailMessage | ||||||
| @ -86,7 +87,8 @@ class EmailStageView(ChallengeStageView): | |||||||
|                 user=pending_user, |                 user=pending_user, | ||||||
|                 identifier=identifier, |                 identifier=identifier, | ||||||
|                 flow=self.executor.flow, |                 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() |         token = tokens.first() | ||||||
|         # Check if token is expired and rotate key if so |         # 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 |         # Because we use the Message-ID as UID for the task, manually assign it | ||||||
|         message_object.extra_headers["Message-ID"] = message_id |         message_object.extra_headers["Message-ID"] = message_id | ||||||
|  |  | ||||||
|         # Add the logo (we can't add it in the previous message since MIMEImage |         # Add the logo if it is used in the email body (we can't add it in the | ||||||
|         # can't be converted to json) |         # 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()) |             message_object.attach(logo_data()) | ||||||
|  |  | ||||||
|         if ( |         if ( | ||||||
|  | |||||||
| @ -96,7 +96,7 @@ | |||||||
|                 <table width="100%" style="background-color: #FFFFFF; border-spacing: 0; margin-top: 15px;"> |                 <table width="100%" style="background-color: #FFFFFF; border-spacing: 0; margin-top: 15px;"> | ||||||
|                   <tr height="80"> |                   <tr height="80"> | ||||||
|                     <td align="center" style="padding: 20px 0;"> |                     <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> |                     </td> | ||||||
|                   </tr> |                   </tr> | ||||||
|                   {% block content %} |                   {% block content %} | ||||||
|  | |||||||
| @ -174,5 +174,5 @@ class TestEmailStageSending(FlowTestCase): | |||||||
|                 response = self.client.post(url) |                 response = self.client.post(url) | ||||||
|             response = self.client.post(url) |             response = self.client.post(url) | ||||||
|             self.assertEqual(response.status_code, 200) |             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") |             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.flows.views.executor import QS_KEY_TOKEN, SESSION_KEY_PLAN, FlowExecutorView | ||||||
| from authentik.lib.config import CONFIG | from authentik.lib.config import CONFIG | ||||||
| from authentik.lib.generators import generate_id | 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.models import EmailStage | ||||||
| from authentik.stages.email.stage import PLAN_CONTEXT_EMAIL_OVERRIDE, EmailStageView | from authentik.stages.email.stage import PLAN_CONTEXT_EMAIL_OVERRIDE, EmailStageView | ||||||
|  |  | ||||||
| @ -160,6 +161,17 @@ class TestEmailStage(FlowTestCase): | |||||||
|                     kwargs={"flow_slug": self.flow.slug}, |                     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.assertEqual(response.status_code, 200) | ||||||
|             self.assertStageRedirects(response, reverse("authentik_core:root-redirect")) |             self.assertStageRedirects(response, reverse("authentik_core:root-redirect")) | ||||||
| @ -182,6 +194,7 @@ class TestEmailStage(FlowTestCase): | |||||||
|         # Set flow token user to a different user |         # Set flow token user to a different user | ||||||
|         token: FlowToken = FlowToken.objects.get(user=self.user) |         token: FlowToken = FlowToken.objects.get(user=self.user) | ||||||
|         token.user = create_test_admin_user() |         token.user = create_test_admin_user() | ||||||
|  |         token.revoke_on_execution = True | ||||||
|         token.save() |         token.save() | ||||||
|  |  | ||||||
|         with patch("authentik.flows.views.executor.FlowExecutorView.cancel", MagicMock()): |         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") |         path = Path("web/dist/assets/icons/icon_left_brand.png") | ||||||
|     with open(path, "rb") as _logo_file: |     with open(path, "rb") as _logo_file: | ||||||
|         logo = MIMEImage(_logo_file.read()) |         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 |     return logo | ||||||
|  |  | ||||||
|  |  | ||||||
|  | |||||||
| @ -101,9 +101,9 @@ class BoundSessionMiddleware(SessionMiddleware): | |||||||
|             SESSION_KEY_BINDING_GEO, GeoIPBinding.NO_BINDING |             SESSION_KEY_BINDING_GEO, GeoIPBinding.NO_BINDING | ||||||
|         ) |         ) | ||||||
|         if configured_binding_net != NetworkBinding.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: |         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 |         # If we got to this point without any error being raised, we need to | ||||||
|         # update the last saved IP to the current one |         # 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: |         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) |             # (== basically requires the user to be logged in) | ||||||
|             request.session[request.session.model.Keys.LAST_IP] = new_ip |             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""" |         """Check network/ASN binding""" | ||||||
|         last_asn = ASN_CONTEXT_PROCESSOR.asn(last_ip) |         last_asn = ASN_CONTEXT_PROCESSOR.asn(last_ip) | ||||||
|         new_asn = ASN_CONTEXT_PROCESSOR.asn(new_ip) |         new_asn = ASN_CONTEXT_PROCESSOR.asn(new_ip) | ||||||
| @ -158,7 +159,8 @@ class BoundSessionMiddleware(SessionMiddleware): | |||||||
|                     new_ip, |                     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""" |         """Check GeoIP binding""" | ||||||
|         last_geo = GEOIP_CONTEXT_PROCESSOR.city(last_ip) |         last_geo = GEOIP_CONTEXT_PROCESSOR.city(last_ip) | ||||||
|         new_geo = GEOIP_CONTEXT_PROCESSOR.city(new_ip) |         new_geo = GEOIP_CONTEXT_PROCESSOR.city(new_ip) | ||||||
| @ -179,8 +181,8 @@ class BoundSessionMiddleware(SessionMiddleware): | |||||||
|             if last_geo.continent != new_geo.continent: |             if last_geo.continent != new_geo.continent: | ||||||
|                 raise SessionBindingBroken( |                 raise SessionBindingBroken( | ||||||
|                     "geoip.continent", |                     "geoip.continent", | ||||||
|                     last_geo.continent, |                     last_geo.continent.to_dict(), | ||||||
|                     new_geo.continent, |                     new_geo.continent.to_dict(), | ||||||
|                     last_ip, |                     last_ip, | ||||||
|                     new_ip, |                     new_ip, | ||||||
|                 ) |                 ) | ||||||
| @ -192,8 +194,8 @@ class BoundSessionMiddleware(SessionMiddleware): | |||||||
|             if last_geo.country != new_geo.country: |             if last_geo.country != new_geo.country: | ||||||
|                 raise SessionBindingBroken( |                 raise SessionBindingBroken( | ||||||
|                     "geoip.country", |                     "geoip.country", | ||||||
|                     last_geo.country, |                     last_geo.country.to_dict(), | ||||||
|                     new_geo.country, |                     new_geo.country.to_dict(), | ||||||
|                     last_ip, |                     last_ip, | ||||||
|                     new_ip, |                     new_ip, | ||||||
|                 ) |                 ) | ||||||
| @ -202,8 +204,8 @@ class BoundSessionMiddleware(SessionMiddleware): | |||||||
|             if last_geo.city != new_geo.city: |             if last_geo.city != new_geo.city: | ||||||
|                 raise SessionBindingBroken( |                 raise SessionBindingBroken( | ||||||
|                     "geoip.city", |                     "geoip.city", | ||||||
|                     last_geo.city, |                     last_geo.city.to_dict(), | ||||||
|                     new_geo.city, |                     new_geo.city.to_dict(), | ||||||
|                     last_ip, |                     last_ip, | ||||||
|                     new_ip, |                     new_ip, | ||||||
|                 ) |                 ) | ||||||
|  | |||||||
| @ -11,7 +11,7 @@ from rest_framework.fields import BooleanField, CharField | |||||||
| from authentik.core.models import Session, User | from authentik.core.models import Session, User | ||||||
| from authentik.events.middleware import audit_ignore | from authentik.events.middleware import audit_ignore | ||||||
| from authentik.flows.challenge import ChallengeResponse, WithUserInfoChallenge | 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.flows.stage import ChallengeStageView | ||||||
| from authentik.lib.utils.time import timedelta_from_string | from authentik.lib.utils.time import timedelta_from_string | ||||||
| from authentik.root.middleware import ClientIPMiddleware | from authentik.root.middleware import ClientIPMiddleware | ||||||
| @ -108,10 +108,6 @@ class UserLoginStageView(ChallengeStageView): | |||||||
|             flow_slug=self.executor.flow.slug, |             flow_slug=self.executor.flow.slug, | ||||||
|             session_duration=delta, |             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: |         if self.executor.current_stage.terminate_other_sessions: | ||||||
|             Session.objects.filter( |             Session.objects.filter( | ||||||
|                 authenticatedsession__user=user, |                 authenticatedsession__user=user, | ||||||
|  | |||||||
| @ -3,6 +3,7 @@ | |||||||
| from time import sleep | from time import sleep | ||||||
| from unittest.mock import patch | from unittest.mock import patch | ||||||
|  |  | ||||||
|  | from django.http import HttpRequest | ||||||
| from django.urls import reverse | from django.urls import reverse | ||||||
| from django.utils.timezone import now | 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.generators import generate_id | ||||||
| from authentik.lib.utils.time import timedelta_from_string | from authentik.lib.utils.time import timedelta_from_string | ||||||
| from authentik.root.middleware import ClientIPMiddleware | 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): | class TestUserLoginStage(FlowTestCase): | ||||||
| @ -192,3 +198,52 @@ class TestUserLoginStage(FlowTestCase): | |||||||
|         self.assertStageRedirects(response, reverse("authentik_core:root-redirect")) |         self.assertStageRedirects(response, reverse("authentik_core:root-redirect")) | ||||||
|         response = self.client.get(reverse("authentik_api:application-list")) |         response = self.client.get(reverse("authentik_api:application-list")) | ||||||
|         self.assertEqual(response.status_code, 403) |         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", |     "$schema": "http://json-schema.org/draft-07/schema", | ||||||
|     "$id": "https://goauthentik.io/blueprints/schema.json", |     "$id": "https://goauthentik.io/blueprints/schema.json", | ||||||
|     "type": "object", |     "type": "object", | ||||||
|     "title": "authentik 2025.4.1 Blueprint schema", |     "title": "authentik 2025.6.3 Blueprint schema", | ||||||
|     "required": [ |     "required": [ | ||||||
|         "version", |         "version", | ||||||
|         "entries" |         "entries" | ||||||
| @ -8147,6 +8147,12 @@ | |||||||
|                     "title": "Group membership field", |                     "title": "Group membership field", | ||||||
|                     "description": "Field which contains members of a group." |                     "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": { |                 "object_uniqueness_field": { | ||||||
|                     "type": "string", |                     "type": "string", | ||||||
|                     "minLength": 1, |                     "minLength": 1, | ||||||
| @ -8180,6 +8186,11 @@ | |||||||
|                     "type": "boolean", |                     "type": "boolean", | ||||||
|                     "title": "Lookup groups from user", |                     "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" |                     "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": [] |             "required": [] | ||||||
|  | |||||||
| @ -31,7 +31,7 @@ services: | |||||||
|     volumes: |     volumes: | ||||||
|       - redis:/data |       - redis:/data | ||||||
|   server: |   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 |     restart: unless-stopped | ||||||
|     command: server |     command: server | ||||||
|     environment: |     environment: | ||||||
| @ -55,7 +55,7 @@ services: | |||||||
|       redis: |       redis: | ||||||
|         condition: service_healthy |         condition: service_healthy | ||||||
|   worker: |   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 |     restart: unless-stopped | ||||||
|     command: worker |     command: worker | ||||||
|     environment: |     environment: | ||||||
|  | |||||||
							
								
								
									
										2
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								go.mod
									
									
									
									
									
								
							| @ -27,7 +27,7 @@ require ( | |||||||
| 	github.com/spf13/cobra v1.9.1 | 	github.com/spf13/cobra v1.9.1 | ||||||
| 	github.com/stretchr/testify v1.10.0 | 	github.com/stretchr/testify v1.10.0 | ||||||
| 	github.com/wwt/guac v1.3.2 | 	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/exp v0.0.0-20230210204819-062eb4c674ab | ||||||
| 	golang.org/x/oauth2 v0.30.0 | 	golang.org/x/oauth2 v0.30.0 | ||||||
| 	golang.org/x/sync v0.14.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.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 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= | ||||||
| go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= | 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.4 h1:cGqzWYnUHrWDoaXWDpIL/kWnX9sFrIhkYDye0P0OEAo= | ||||||
| goauthentik.io/api/v3 v3.2025041.2/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw= | 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-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= | ||||||
| golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= | ||||||
| golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= | ||||||
|  | |||||||
| @ -33,4 +33,4 @@ func UserAgent() string { | |||||||
| 	return fmt.Sprintf("authentik@%s", FullVersion()) | 	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, | 		si:  si, | ||||||
| 		log: log.WithField("logger", "authentik.outpost.ldap.binder.session"), | 		log: log.WithField("logger", "authentik.outpost.ldap.binder.session"), | ||||||
| 	} | 	} | ||||||
|  | 	if oldBinder != nil { | ||||||
| 		if oldSb, ok := oldBinder.(*SessionBinder); ok { | 		if oldSb, ok := oldBinder.(*SessionBinder); ok { | ||||||
| 			sb.DirectBinder = oldSb.DirectBinder | 			sb.DirectBinder = oldSb.DirectBinder | ||||||
| 			sb.sessions = oldSb.sessions | 			sb.sessions = oldSb.sessions | ||||||
| 			sb.log.Debug("re-initialised session binder") | 			sb.log.Debug("re-initialised session binder") | ||||||
| 	} else { | 			return sb | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
| 	sb.sessions = ttlcache.New(ttlcache.WithDisableTouchOnHit[Credentials, ldap.LDAPResultCode]()) | 	sb.sessions = ttlcache.New(ttlcache.WithDisableTouchOnHit[Credentials, ldap.LDAPResultCode]()) | ||||||
| 	sb.DirectBinder = *direct.NewDirectBinder(si) | 	sb.DirectBinder = *direct.NewDirectBinder(si) | ||||||
| 	go sb.sessions.Start() | 	go sb.sessions.Start() | ||||||
| 	sb.log.Debug("initialised session binder") | 	sb.log.Debug("initialised session binder") | ||||||
| 	} |  | ||||||
| 	return sb | 	return sb | ||||||
| } | } | ||||||
|  |  | ||||||
|  | |||||||
| @ -16,6 +16,7 @@ import ( | |||||||
| 	memorybind "goauthentik.io/internal/outpost/ldap/bind/memory" | 	memorybind "goauthentik.io/internal/outpost/ldap/bind/memory" | ||||||
| 	"goauthentik.io/internal/outpost/ldap/constants" | 	"goauthentik.io/internal/outpost/ldap/constants" | ||||||
| 	"goauthentik.io/internal/outpost/ldap/flags" | 	"goauthentik.io/internal/outpost/ldap/flags" | ||||||
|  | 	"goauthentik.io/internal/outpost/ldap/search" | ||||||
| 	directsearch "goauthentik.io/internal/outpost/ldap/search/direct" | 	directsearch "goauthentik.io/internal/outpost/ldap/search/direct" | ||||||
| 	memorysearch "goauthentik.io/internal/outpost/ldap/search/memory" | 	memorysearch "goauthentik.io/internal/outpost/ldap/search/memory" | ||||||
| ) | ) | ||||||
| @ -85,7 +86,11 @@ func (ls *LDAPServer) Refresh() error { | |||||||
| 			providers[idx].certUUID = *kp | 			providers[idx].certUUID = *kp | ||||||
| 		} | 		} | ||||||
| 		if *provider.SearchMode.Ptr() == api.LDAPAPIACCESSMODE_CACHED { | 		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 { | 		} else if *provider.SearchMode.Ptr() == api.LDAPAPIACCESSMODE_DIRECT { | ||||||
| 			providers[idx].searcher = directsearch.NewDirectSearcher(providers[idx]) | 			providers[idx].searcher = directsearch.NewDirectSearcher(providers[idx]) | ||||||
| 		} | 		} | ||||||
|  | |||||||
| @ -31,13 +31,26 @@ type MemorySearcher struct { | |||||||
| 	groups []api.Group | 	groups []api.Group | ||||||
| } | } | ||||||
|  |  | ||||||
| func NewMemorySearcher(si server.LDAPServerInstance) *MemorySearcher { | func NewMemorySearcher(si server.LDAPServerInstance, existing search.Searcher) *MemorySearcher { | ||||||
| 	ms := &MemorySearcher{ | 	ms := &MemorySearcher{ | ||||||
| 		si:  si, | 		si:  si, | ||||||
| 		log: log.WithField("logger", "authentik.outpost.ldap.searcher.memory"), | 		log: log.WithField("logger", "authentik.outpost.ldap.searcher.memory"), | ||||||
| 		ds:  direct.NewDirectSearcher(si), | 		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") | 	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 | 	// 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{ | 	users, _ := ak.Paginator(ms.si.GetAPIClient().CoreApi.CoreUsersList(context.TODO()).IncludeGroups(true), ak.PaginatorOptions{ | ||||||
| 		PageSize: 100, | 		PageSize: 100, | ||||||
| @ -49,7 +62,6 @@ func NewMemorySearcher(si server.LDAPServerInstance) *MemorySearcher { | |||||||
| 		Logger:   ms.log, | 		Logger:   ms.log, | ||||||
| 	}) | 	}) | ||||||
| 	ms.groups = groups | 	ms.groups = groups | ||||||
| 	return ms |  | ||||||
| } | } | ||||||
|  |  | ||||||
| func (ms *MemorySearcher) SearchBase(req *search.Request) (ldap.ServerSearchResult, error) { | func (ms *MemorySearcher) SearchBase(req *search.Request) (ldap.ServerSearchResult, error) { | ||||||
|  | |||||||
| @ -5,6 +5,7 @@ import ( | |||||||
| 	"crypto/sha256" | 	"crypto/sha256" | ||||||
| 	"crypto/tls" | 	"crypto/tls" | ||||||
| 	"encoding/gob" | 	"encoding/gob" | ||||||
|  | 	"encoding/hex" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"html/template" | 	"html/template" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| @ -118,8 +119,8 @@ func NewApplication(p api.ProxyOutpostConfig, c *http.Client, server Server, old | |||||||
| 	mux := mux.NewRouter() | 	mux := mux.NewRouter() | ||||||
|  |  | ||||||
| 	// Save cookie name, based on hashed client ID | 	// Save cookie name, based on hashed client ID | ||||||
| 	h := sha256.New() | 	hs := sha256.Sum256([]byte(*p.ClientId)) | ||||||
| 	bs := string(h.Sum([]byte(*p.ClientId))) | 	bs := hex.EncodeToString(hs[:]) | ||||||
| 	sessionName := fmt.Sprintf("authentik_proxy_%s", bs[:8]) | 	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 | 	// 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 { | type ProxyClaims struct { | ||||||
| 	UserAttributes  map[string]interface{} `json:"user_attributes"` | 	UserAttributes  map[string]interface{} `json:"user_attributes"` | ||||||
| 	BackendOverride string                 `json:"backend_override"` | 	BackendOverride string                 `json:"backend_override"` | ||||||
|  | 	HostHeader      string                 `json:"host_header"` | ||||||
| 	IsSuperuser     bool                   `json:"is_superuser"` | 	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.Scheme = ou.Scheme | ||||||
| 		r.URL.Host = ou.Host | 		r.URL.Host = ou.Host | ||||||
| 		claims := a.getClaimsFromSession(r) | 		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) | 				u, err := url.Parse(claims.Proxy.BackendOverride) | ||||||
| 				if err != nil { | 				if err != nil { | ||||||
| 					a.log.WithField("backend_override", claims.Proxy.BackendOverride).WithError(err).Warning("failed parse user backend override") | 					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 | 					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") | 		a.log.WithField("upstream_url", r.URL.String()).Trace("final upstream url") | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  | |||||||
| @ -2,6 +2,7 @@ package radius | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"crypto/sha512" | 	"crypto/sha512" | ||||||
|  | 	"encoding/hex" | ||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
| 	"github.com/getsentry/sentry-go" | 	"github.com/getsentry/sentry-go" | ||||||
| @ -68,7 +69,9 @@ func (rs *RadiusServer) ServeRADIUS(w radius.ResponseWriter, r *radius.Request) | |||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 	if pi == nil { | 	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)) | 		_ = w.Write(r.Response(radius.CodeAccessReject)) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  | |||||||
| @ -67,11 +67,15 @@ func (ws *WebServer) configureStatic() { | |||||||
|  |  | ||||||
| 	// Media files, if backend is file | 	// Media files, if backend is file | ||||||
| 	if config.Get().Storage.Media.Backend == "file" { | 	if config.Get().Storage.Media.Backend == "file" { | ||||||
| 		fsMedia := http.StripPrefix("/media", http.FileServer(http.Dir(config.Get().Storage.Media.File.Path))) | 		fsMedia := 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) { | 		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") | 				w.Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'; sandbox") | ||||||
| 				fsMedia.ServeHTTP(w, r) | 				fsMedia.ServeHTTP(w, r) | ||||||
| 		}) | 			}), | ||||||
|  | 			"media/", | ||||||
|  | 			config.Get().Web.Path, | ||||||
|  | 		)) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	staticRouter.PathPrefix(config.Get().Web.Path).PathPrefix("/if/help/").Handler(pathStripper( | 	staticRouter.PathPrefix(config.Get().Web.Path).PathPrefix("/if/help/").Handler(pathStripper( | ||||||
|  | |||||||
| @ -26,7 +26,7 @@ Parameters: | |||||||
|     Description: authentik Docker image |     Description: authentik Docker image | ||||||
|   AuthentikVersion: |   AuthentikVersion: | ||||||
|     Type: String |     Type: String | ||||||
|     Default: 2025.4.1 |     Default: 2025.6.3 | ||||||
|     Description: authentik Docker image tag |     Description: authentik Docker image tag | ||||||
|   AuthentikServerCPU: |   AuthentikServerCPU: | ||||||
|     Type: Number |     Type: Number | ||||||
|  | |||||||
										
											Binary file not shown.
										
									
								
							| @ -32,15 +32,17 @@ | |||||||
| # datenschmutz, 2025 | # datenschmutz, 2025 | ||||||
| # 97cce0ae0cad2a2cc552d3165d04643e_de3d740, 2025 | # 97cce0ae0cad2a2cc552d3165d04643e_de3d740, 2025 | ||||||
| # Dominic Wagner <mail@dominic-wagner.de>, 2025 | # Dominic Wagner <mail@dominic-wagner.de>, 2025 | ||||||
|  | # Till-Frederik Riechard, 2025 | ||||||
|  | # Alexander Mnich, 2025 | ||||||
| #  | #  | ||||||
| #, fuzzy | #, fuzzy | ||||||
| msgid "" | msgid "" | ||||||
| msgstr "" | msgstr "" | ||||||
| "Project-Id-Version: PACKAGE VERSION\n" | "Project-Id-Version: PACKAGE VERSION\n" | ||||||
| "Report-Msgid-Bugs-To: \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" | "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" | "Language-Team: German (https://app.transifex.com/authentik/teams/119923/de/)\n" | ||||||
| "MIME-Version: 1.0\n" | "MIME-Version: 1.0\n" | ||||||
| "Content-Type: text/plain; charset=UTF-8\n" | "Content-Type: text/plain; charset=UTF-8\n" | ||||||
| @ -132,6 +134,10 @@ msgstr "" | |||||||
| msgid "Web Certificate used by the authentik Core webserver." | msgid "Web Certificate used by the authentik Core webserver." | ||||||
| msgstr "Vom Authentik-Core-Webserver verwendetes Zertifikat." | msgstr "Vom Authentik-Core-Webserver verwendetes Zertifikat." | ||||||
|  |  | ||||||
|  | #: authentik/brands/models.py | ||||||
|  | msgid "Certificates used for client authentication." | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: authentik/brands/models.py | #: authentik/brands/models.py | ||||||
| msgid "Brand" | msgid "Brand" | ||||||
| msgstr "Marke" | msgstr "Marke" | ||||||
| @ -405,7 +411,7 @@ msgstr "Eigenschaften" | |||||||
|  |  | ||||||
| #: authentik/core/models.py | #: authentik/core/models.py | ||||||
| msgid "session data" | msgid "session data" | ||||||
| msgstr "" | msgstr "Sitzungsdaten" | ||||||
|  |  | ||||||
| #: authentik/core/models.py | #: authentik/core/models.py | ||||||
| msgid "Session" | msgid "Session" | ||||||
| @ -533,7 +539,7 @@ msgstr "" | |||||||
|  |  | ||||||
| #: authentik/enterprise/policies/unique_password/models.py | #: authentik/enterprise/policies/unique_password/models.py | ||||||
| msgid "Number of passwords to check against." | 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/enterprise/policies/unique_password/models.py | ||||||
| #: authentik/policies/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 | #: authentik/enterprise/policies/unique_password/models.py | ||||||
| msgid "This password has been used previously. Please choose a different one." | msgid "This password has been used previously. Please choose a different one." | ||||||
| msgstr "" | msgstr "" | ||||||
|  | "Dieses Passwort wurde in Vergangenheit bereits verwendet. Bitte nutzen Sie " | ||||||
|  | "ein anderes." | ||||||
|  |  | ||||||
| #: authentik/enterprise/policies/unique_password/models.py | #: authentik/enterprise/policies/unique_password/models.py | ||||||
| msgid "Password Uniqueness Policy" | msgid "Password Uniqueness Policy" | ||||||
| msgstr "" | msgstr "Passwort-Einzigartigkeits-Richtlinie" | ||||||
|  |  | ||||||
| #: authentik/enterprise/policies/unique_password/models.py | #: authentik/enterprise/policies/unique_password/models.py | ||||||
| msgid "Password Uniqueness Policies" | msgid "Password Uniqueness Policies" | ||||||
| msgstr "" | msgstr "Passwort-Einzigartigkeits-Richtlinien" | ||||||
|  |  | ||||||
| #: authentik/enterprise/policies/unique_password/models.py | #: authentik/enterprise/policies/unique_password/models.py | ||||||
| msgid "User Password History" | msgid "User Password History" | ||||||
| msgstr "" | msgstr "Nutzer-Passwort-Historie" | ||||||
|  |  | ||||||
| #: authentik/enterprise/policy.py | #: authentik/enterprise/policy.py | ||||||
| msgid "Enterprise required to access this feature." | msgid "Enterprise required to access this feature." | ||||||
| @ -693,6 +701,33 @@ msgstr "Endgeräte" | |||||||
| msgid "Verifying your browser..." | msgid "Verifying your browser..." | ||||||
| msgstr "Verifiziere deinen 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 | #: authentik/enterprise/stages/source/models.py | ||||||
| msgid "" | msgid "" | ||||||
| "Amount of time a user can take to return from the source to continue the " | "Amount of time a user can take to return from the source to continue the " | ||||||
| @ -988,7 +1023,7 @@ msgstr "" | |||||||
|  |  | ||||||
| #: authentik/flows/models.py | #: authentik/flows/models.py | ||||||
| msgid "Evaluate policies when the Stage is presented to the user." | 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 | #: authentik/flows/models.py | ||||||
| msgid "" | msgid "" | ||||||
| @ -1043,9 +1078,12 @@ msgid "Starting full provider sync" | |||||||
| msgstr "Starte komplette Provider Synchronisation." | msgstr "Starte komplette Provider Synchronisation." | ||||||
|  |  | ||||||
| #: authentik/lib/sync/outgoing/tasks.py | #: authentik/lib/sync/outgoing/tasks.py | ||||||
| #, python-brace-format | msgid "Syncing users" | ||||||
| msgid "Syncing page {page} of users" | msgstr "" | ||||||
| msgstr "Synchonisiere Benutzer Seite {page}" |  | ||||||
|  | #: authentik/lib/sync/outgoing/tasks.py | ||||||
|  | msgid "Syncing groups" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: authentik/lib/sync/outgoing/tasks.py | #: authentik/lib/sync/outgoing/tasks.py | ||||||
| #, python-brace-format | #, python-brace-format | ||||||
| @ -1593,11 +1631,11 @@ msgstr "ES256 (Asymmetrische Verschlüsselung)" | |||||||
|  |  | ||||||
| #: authentik/providers/oauth2/models.py | #: authentik/providers/oauth2/models.py | ||||||
| msgid "ES384 (Asymmetric Encryption)" | msgid "ES384 (Asymmetric Encryption)" | ||||||
| msgstr "" | msgstr "ES384 (Asymmetrische Verschlüsselung)" | ||||||
|  |  | ||||||
| #: authentik/providers/oauth2/models.py | #: authentik/providers/oauth2/models.py | ||||||
| msgid "ES512 (Asymmetric Encryption)" | msgid "ES512 (Asymmetric Encryption)" | ||||||
| msgstr "" | msgstr "ES5122 (Asymmetrische Verschlüsselung)" | ||||||
|  |  | ||||||
| #: authentik/providers/oauth2/models.py | #: authentik/providers/oauth2/models.py | ||||||
| msgid "Scope used by the client" | msgid "Scope used by the client" | ||||||
| @ -2183,11 +2221,11 @@ msgstr "Standard" | |||||||
|  |  | ||||||
| #: authentik/providers/scim/models.py | #: authentik/providers/scim/models.py | ||||||
| msgid "AWS" | msgid "AWS" | ||||||
| msgstr "" | msgstr "AWS" | ||||||
|  |  | ||||||
| #: authentik/providers/scim/models.py | #: authentik/providers/scim/models.py | ||||||
| msgid "Slack" | msgid "Slack" | ||||||
| msgstr "" | msgstr "Slack" | ||||||
|  |  | ||||||
| #: authentik/providers/scim/models.py | #: authentik/providers/scim/models.py | ||||||
| msgid "Base URL to SCIM requests, usually ends in /v2" | msgid "Base URL to SCIM requests, usually ends in /v2" | ||||||
| @ -2199,7 +2237,7 @@ msgstr "Authentifizierungstoken" | |||||||
|  |  | ||||||
| #: authentik/providers/scim/models.py | #: authentik/providers/scim/models.py | ||||||
| msgid "SCIM Compatibility Mode" | msgid "SCIM Compatibility Mode" | ||||||
| msgstr "" | msgstr "SCIM Kompatibilitätsmodus" | ||||||
|  |  | ||||||
| #: authentik/providers/scim/models.py | #: authentik/providers/scim/models.py | ||||||
| msgid "Alter authentik behavior for vendor-specific SCIM implementations." | msgid "Alter authentik behavior for vendor-specific SCIM implementations." | ||||||
| @ -2231,7 +2269,7 @@ msgstr "Rollen" | |||||||
|  |  | ||||||
| #: authentik/rbac/models.py | #: authentik/rbac/models.py | ||||||
| msgid "Initial Permissions" | msgid "Initial Permissions" | ||||||
| msgstr "" | msgstr "Initiale Berechtigungen" | ||||||
|  |  | ||||||
| #: authentik/rbac/models.py | #: authentik/rbac/models.py | ||||||
| msgid "System permission" | msgid "System permission" | ||||||
| @ -2487,6 +2525,12 @@ msgid "" | |||||||
| "Active Directory" | "Active Directory" | ||||||
| msgstr "" | 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 | #: authentik/sources/ldap/models.py | ||||||
| msgid "LDAP Source" | msgid "LDAP Source" | ||||||
| msgstr "LDAP Quelle" | msgstr "LDAP Quelle" | ||||||
| @ -2504,20 +2548,25 @@ msgid "LDAP Source Property Mappings" | |||||||
| msgstr "LDAP Quelle Eigenschafts-Zuordnungen" | msgstr "LDAP Quelle Eigenschafts-Zuordnungen" | ||||||
|  |  | ||||||
| #: authentik/sources/ldap/models.py | #: 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 "" | msgstr "" | ||||||
|  |  | ||||||
|  | #: authentik/sources/ldap/models.py | ||||||
|  | msgid "User LDAP Source Connection" | ||||||
|  | msgstr "Benutzer LDAP-Quellverbindung" | ||||||
|  |  | ||||||
| #: authentik/sources/ldap/models.py | #: authentik/sources/ldap/models.py | ||||||
| msgid "User LDAP Source Connections" | msgid "User LDAP Source Connections" | ||||||
| msgstr "" | msgstr "Benutzer LDAP-Quellverbindungen" | ||||||
|  |  | ||||||
| #: authentik/sources/ldap/models.py | #: authentik/sources/ldap/models.py | ||||||
| msgid "Group LDAP Source Connection" | msgid "Group LDAP Source Connection" | ||||||
| msgstr "" | msgstr "LDAP Gruppen Quellverbindung" | ||||||
|  |  | ||||||
| #: authentik/sources/ldap/models.py | #: authentik/sources/ldap/models.py | ||||||
| msgid "Group LDAP Source Connections" | msgid "Group LDAP Source Connections" | ||||||
| msgstr "" | msgstr "LDAP Gruppen Quellverbindungen" | ||||||
|  |  | ||||||
| #: authentik/sources/ldap/signals.py | #: authentik/sources/ldap/signals.py | ||||||
| msgid "Password does not match Active Directory Complexity." | msgid "Password does not match Active Directory Complexity." | ||||||
| @ -2530,7 +2579,7 @@ msgstr "Kein Token empfangen." | |||||||
|  |  | ||||||
| #: authentik/sources/oauth/models.py | #: authentik/sources/oauth/models.py | ||||||
| msgid "HTTP Basic Authentication" | msgid "HTTP Basic Authentication" | ||||||
| msgstr "" | msgstr "HTTP Basic Authentifizierung" | ||||||
|  |  | ||||||
| #: authentik/sources/oauth/models.py | #: authentik/sources/oauth/models.py | ||||||
| msgid "Include the client ID and secret as request parameters" | msgid "Include the client ID and secret as request parameters" | ||||||
| @ -2896,6 +2945,11 @@ msgstr "SAML Gruppen Quellverbindung" | |||||||
| msgid "Group SAML Source Connections" | msgid "Group SAML Source Connections" | ||||||
| msgstr "SAML Gruppen Quellverbindungen" | msgstr "SAML Gruppen Quellverbindungen" | ||||||
|  |  | ||||||
|  | #: authentik/sources/saml/views.py | ||||||
|  | #, python-brace-format | ||||||
|  | msgid "Continue to {source_name}" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: authentik/sources/scim/models.py | #: authentik/sources/scim/models.py | ||||||
| msgid "SCIM Source" | msgid "SCIM Source" | ||||||
| msgstr "SCIM Quelle" | msgstr "SCIM Quelle" | ||||||
| @ -2930,7 +2984,7 @@ msgstr "Duo Geräte" | |||||||
|  |  | ||||||
| #: authentik/stages/authenticator_email/models.py | #: authentik/stages/authenticator_email/models.py | ||||||
| msgid "Email OTP" | msgid "Email OTP" | ||||||
| msgstr "" | msgstr "E-Mail Einmalpasswort" | ||||||
|  |  | ||||||
| #: authentik/stages/authenticator_email/models.py | #: authentik/stages/authenticator_email/models.py | ||||||
| #: authentik/stages/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 | #: authentik/stages/authenticator_email/models.py | ||||||
| msgid "Email Device" | msgid "Email Device" | ||||||
| msgstr "" | msgstr "E-Mail Gerät" | ||||||
|  |  | ||||||
| #: authentik/stages/authenticator_email/models.py | #: authentik/stages/authenticator_email/models.py | ||||||
| msgid "Email Devices" | msgid "Email Devices" | ||||||
| msgstr "" | msgstr "E-Mail Geräte" | ||||||
|  |  | ||||||
| #: authentik/stages/authenticator_email/stage.py | #: authentik/stages/authenticator_email/stage.py | ||||||
| #: authentik/stages/authenticator_sms/stage.py | #: authentik/stages/authenticator_sms/stage.py | ||||||
| @ -2977,7 +3031,7 @@ msgstr "Code stimmt nicht überein" | |||||||
|  |  | ||||||
| #: authentik/stages/authenticator_email/stage.py | #: authentik/stages/authenticator_email/stage.py | ||||||
| msgid "Invalid email" | msgid "Invalid email" | ||||||
| msgstr "" | msgstr "Ungültige E-Mail" | ||||||
|  |  | ||||||
| #: authentik/stages/authenticator_email/templates/email/email_otp.html | #: authentik/stages/authenticator_email/templates/email/email_otp.html | ||||||
| #: authentik/stages/email/templates/email/password_reset.html | #: authentik/stages/email/templates/email/password_reset.html | ||||||
| @ -3273,6 +3327,10 @@ msgstr "Zustimmung der Benutzer" | |||||||
| msgid "User Consents" | msgid "User Consents" | ||||||
| msgstr "Zustimmungen der Benutzer" | msgstr "Zustimmungen der Benutzer" | ||||||
|  |  | ||||||
|  | #: authentik/stages/consent/stage.py | ||||||
|  | msgid "Invalid consent token, re-showing prompt" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: authentik/stages/deny/models.py | #: authentik/stages/deny/models.py | ||||||
| msgid "Deny Stage" | msgid "Deny Stage" | ||||||
| msgstr "Verweigerungsstufe" | msgstr "Verweigerungsstufe" | ||||||
| @ -3289,6 +3347,14 @@ msgstr "Dummy Stufe" | |||||||
| msgid "Dummy Stages" | msgid "Dummy Stages" | ||||||
| msgstr "Dummy Stufen" | 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 | #: authentik/stages/email/models.py | ||||||
| msgid "Password Reset" | msgid "Password Reset" | ||||||
| msgstr "Passwort zurücksetzen" | msgstr "Passwort zurücksetzen" | ||||||
| @ -3890,10 +3956,11 @@ msgstr "" | |||||||
| #: authentik/tenants/models.py | #: authentik/tenants/models.py | ||||||
| msgid "Reputation cannot decrease lower than this value. Zero or negative." | msgid "Reputation cannot decrease lower than this value. Zero or negative." | ||||||
| msgstr "" | msgstr "" | ||||||
|  | "Reputation kann nicht niedriger als dieser Wert sein. Null oder negativ." | ||||||
|  |  | ||||||
| #: authentik/tenants/models.py | #: authentik/tenants/models.py | ||||||
| msgid "Reputation cannot increase higher than this value. Zero or positive." | 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 | #: authentik/tenants/models.py | ||||||
| msgid "The option configures the footer links on the flow executor pages." | msgid "The option configures the footer links on the flow executor pages." | ||||||
|  | |||||||
| @ -8,7 +8,7 @@ msgid "" | |||||||
| msgstr "" | msgstr "" | ||||||
| "Project-Id-Version: PACKAGE VERSION\n" | "Project-Id-Version: PACKAGE VERSION\n" | ||||||
| "Report-Msgid-Bugs-To: \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" | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" | ||||||
| "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" | "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" | ||||||
| "Language-Team: LANGUAGE <LL@li.org>\n" | "Language-Team: LANGUAGE <LL@li.org>\n" | ||||||
| @ -961,8 +961,11 @@ msgid "Starting full provider sync" | |||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: authentik/lib/sync/outgoing/tasks.py | #: authentik/lib/sync/outgoing/tasks.py | ||||||
| #, python-brace-format | msgid "Syncing users" | ||||||
| msgid "Syncing page {page} of users" | msgstr "" | ||||||
|  |  | ||||||
|  | #: authentik/lib/sync/outgoing/tasks.py | ||||||
|  | msgid "Syncing groups" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: authentik/lib/sync/outgoing/tasks.py | #: authentik/lib/sync/outgoing/tasks.py | ||||||
| @ -2223,6 +2226,10 @@ msgstr "" | |||||||
| msgid "Consider Objects matching this filter to be Users." | msgid "Consider Objects matching this filter to be Users." | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
|  | #: authentik/sources/ldap/models.py | ||||||
|  | msgid "Attribute which matches the value of `group_membership_field`." | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: authentik/sources/ldap/models.py | #: authentik/sources/ldap/models.py | ||||||
| msgid "Field which contains members of a group." | msgid "Field which contains members of a group." | ||||||
| msgstr "" | msgstr "" | ||||||
| @ -2252,6 +2259,12 @@ msgid "" | |||||||
| "Active Directory" | "Active Directory" | ||||||
| msgstr "" | 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 | #: authentik/sources/ldap/models.py | ||||||
| msgid "LDAP Source" | msgid "LDAP Source" | ||||||
| msgstr "" | msgstr "" | ||||||
| @ -2268,6 +2281,11 @@ msgstr "" | |||||||
| msgid "LDAP Source Property Mappings" | msgid "LDAP Source Property Mappings" | ||||||
| msgstr "" | 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 | #: authentik/sources/ldap/models.py | ||||||
| msgid "User LDAP Source Connection" | msgid "User LDAP Source Connection" | ||||||
| msgstr "" | msgstr "" | ||||||
| @ -2639,6 +2657,11 @@ msgstr "" | |||||||
| msgid "Group SAML Source Connections" | msgid "Group SAML Source Connections" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
|  | #: authentik/sources/saml/views.py | ||||||
|  | #, python-brace-format | ||||||
|  | msgid "Continue to {source_name}" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: authentik/sources/scim/models.py | #: authentik/sources/scim/models.py | ||||||
| msgid "SCIM Source" | msgid "SCIM Source" | ||||||
| msgstr "" | msgstr "" | ||||||
| @ -2994,6 +3017,10 @@ msgstr "" | |||||||
| msgid "User Consents" | msgid "User Consents" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
|  | #: authentik/stages/consent/stage.py | ||||||
|  | msgid "Invalid consent token, re-showing prompt" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: authentik/stages/deny/models.py | #: authentik/stages/deny/models.py | ||||||
| msgid "Deny Stage" | msgid "Deny Stage" | ||||||
| msgstr "" | msgstr "" | ||||||
| @ -3010,6 +3037,14 @@ msgstr "" | |||||||
| msgid "Dummy Stages" | msgid "Dummy Stages" | ||||||
| msgstr "" | 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 | #: authentik/stages/email/models.py | ||||||
| msgid "Password Reset" | msgid "Password Reset" | ||||||
| msgstr "" | msgstr "" | ||||||
| @ -3462,10 +3497,6 @@ msgstr "" | |||||||
| msgid "No Pending user to login." | msgid "No Pending user to login." | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: authentik/stages/user_login/stage.py |  | ||||||
| msgid "Successfully logged in!" |  | ||||||
| msgstr "" |  | ||||||
|  |  | ||||||
| #: authentik/stages/user_logout/models.py | #: authentik/stages/user_logout/models.py | ||||||
| msgid "User Logout Stage" | msgid "User Logout Stage" | ||||||
| msgstr "" | msgstr "" | ||||||
|  | |||||||
										
											Binary file not shown.
										
									
								
							| @ -15,7 +15,7 @@ msgid "" | |||||||
| msgstr "" | msgstr "" | ||||||
| "Project-Id-Version: PACKAGE VERSION\n" | "Project-Id-Version: PACKAGE VERSION\n" | ||||||
| "Report-Msgid-Bugs-To: \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" | "PO-Revision-Date: 2022-09-26 16:47+0000\n" | ||||||
| "Last-Translator: Jens L. <jens@goauthentik.io>, 2025\n" | "Last-Translator: Jens L. <jens@goauthentik.io>, 2025\n" | ||||||
| "Language-Team: Spanish (https://app.transifex.com/authentik/teams/119923/es/)\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." | msgid "Web Certificate used by the authentik Core webserver." | ||||||
| msgstr "Certificado Web usado por el servidor web Core de authentik" | 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 | #: authentik/brands/models.py | ||||||
| msgid "Brand" | msgid "Brand" | ||||||
| msgstr "Marca" | msgstr "Marca" | ||||||
| @ -671,6 +675,33 @@ msgstr "Dispositivos de Punto de Conexión" | |||||||
| msgid "Verifying your browser..." | msgid "Verifying your browser..." | ||||||
| msgstr "Verificando tu navegador..." | 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 | #: authentik/enterprise/stages/source/models.py | ||||||
| msgid "" | msgid "" | ||||||
| "Amount of time a user can take to return from the source to continue the " | "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" | msgstr "Iniciando sincronización completa de proveedor" | ||||||
|  |  | ||||||
| #: authentik/lib/sync/outgoing/tasks.py | #: authentik/lib/sync/outgoing/tasks.py | ||||||
| #, python-brace-format | msgid "Syncing users" | ||||||
| msgid "Syncing page {page} of users" | msgstr "" | ||||||
| msgstr "Sincronizando página {page} de usuarios" |  | ||||||
|  | #: authentik/lib/sync/outgoing/tasks.py | ||||||
|  | msgid "Syncing groups" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: authentik/lib/sync/outgoing/tasks.py | #: authentik/lib/sync/outgoing/tasks.py | ||||||
| #, python-brace-format | #, python-brace-format | ||||||
| @ -2452,6 +2486,12 @@ msgid "" | |||||||
| "Active Directory" | "Active Directory" | ||||||
| msgstr "" | 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 | #: authentik/sources/ldap/models.py | ||||||
| msgid "LDAP Source" | msgid "LDAP Source" | ||||||
| msgstr "Fuente de LDAP" | msgstr "Fuente de LDAP" | ||||||
| @ -2468,6 +2508,11 @@ msgstr "Asignación de Propiedades de Fuente de LDAP" | |||||||
| msgid "LDAP Source Property Mappings" | msgid "LDAP Source Property Mappings" | ||||||
| msgstr "Asignaciones de Propiedades de Fuente de LDAP" | 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 | #: authentik/sources/ldap/models.py | ||||||
| msgid "User LDAP Source Connection" | msgid "User LDAP Source Connection" | ||||||
| msgstr "" | msgstr "" | ||||||
| @ -2859,6 +2904,11 @@ msgstr "Conexión de Fuente de SAML de Grupo" | |||||||
| msgid "Group SAML Source Connections" | msgid "Group SAML Source Connections" | ||||||
| msgstr "Conexiones de Fuente de SAML de Grupo" | 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 | #: authentik/sources/scim/models.py | ||||||
| msgid "SCIM Source" | msgid "SCIM Source" | ||||||
| msgstr "Fuente de SCIM" | msgstr "Fuente de SCIM" | ||||||
| @ -3245,6 +3295,10 @@ msgstr "Consentimiento del usuario" | |||||||
| msgid "User Consents" | msgid "User Consents" | ||||||
| msgstr "Consentimientos del usuario" | msgstr "Consentimientos del usuario" | ||||||
|  |  | ||||||
|  | #: authentik/stages/consent/stage.py | ||||||
|  | msgid "Invalid consent token, re-showing prompt" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: authentik/stages/deny/models.py | #: authentik/stages/deny/models.py | ||||||
| msgid "Deny Stage" | msgid "Deny Stage" | ||||||
| msgstr "Etapa de denegación" | msgstr "Etapa de denegación" | ||||||
| @ -3261,6 +3315,14 @@ msgstr "Escenario ficticio" | |||||||
| msgid "Dummy Stages" | msgid "Dummy Stages" | ||||||
| msgstr "Etapas ficticias" | 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 | #: authentik/stages/email/models.py | ||||||
| msgid "Password Reset" | msgid "Password Reset" | ||||||
| msgstr "Restablecimiento de contraseña" | msgstr "Restablecimiento de contraseña" | ||||||
|  | |||||||
										
											Binary file not shown.
										
									
								
							| @ -15,7 +15,7 @@ msgid "" | |||||||
| msgstr "" | msgstr "" | ||||||
| "Project-Id-Version: PACKAGE VERSION\n" | "Project-Id-Version: PACKAGE VERSION\n" | ||||||
| "Report-Msgid-Bugs-To: \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" | "PO-Revision-Date: 2022-09-26 16:47+0000\n" | ||||||
| "Last-Translator: Ville Ranki, 2025\n" | "Last-Translator: Ville Ranki, 2025\n" | ||||||
| "Language-Team: Finnish (https://app.transifex.com/authentik/teams/119923/fi/)\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." | msgid "Web Certificate used by the authentik Core webserver." | ||||||
| msgstr "Web-sertifikaatti, jota authentik Core -verkkopalvelin käyttää." | msgstr "Web-sertifikaatti, jota authentik Core -verkkopalvelin käyttää." | ||||||
|  |  | ||||||
|  | #: authentik/brands/models.py | ||||||
|  | msgid "Certificates used for client authentication." | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: authentik/brands/models.py | #: authentik/brands/models.py | ||||||
| msgid "Brand" | msgid "Brand" | ||||||
| msgstr "Brändi" | msgstr "Brändi" | ||||||
| @ -658,6 +662,33 @@ msgstr "Päätelaitteet" | |||||||
| msgid "Verifying your browser..." | msgid "Verifying your browser..." | ||||||
| msgstr "Selaintasi varmennetaan..." | 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 | #: authentik/enterprise/stages/source/models.py | ||||||
| msgid "" | msgid "" | ||||||
| "Amount of time a user can take to return from the source to continue the " | "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" | msgstr "Käynnistetään palveluntarjoajan täysi synkronisointi" | ||||||
|  |  | ||||||
| #: authentik/lib/sync/outgoing/tasks.py | #: authentik/lib/sync/outgoing/tasks.py | ||||||
| #, python-brace-format | msgid "Syncing users" | ||||||
| msgid "Syncing page {page} of users" | msgstr "" | ||||||
| msgstr "Synkronoidaan käyttäjien sivua {page}" |  | ||||||
|  | #: authentik/lib/sync/outgoing/tasks.py | ||||||
|  | msgid "Syncing groups" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: authentik/lib/sync/outgoing/tasks.py | #: authentik/lib/sync/outgoing/tasks.py | ||||||
| #, python-brace-format | #, python-brace-format | ||||||
| @ -2429,6 +2463,12 @@ msgid "" | |||||||
| "Active Directory" | "Active Directory" | ||||||
| msgstr "" | 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 | #: authentik/sources/ldap/models.py | ||||||
| msgid "LDAP Source" | msgid "LDAP Source" | ||||||
| msgstr "LDAP-lähde" | msgstr "LDAP-lähde" | ||||||
| @ -2445,6 +2485,11 @@ msgstr "LDAP-lähteen ominaisuuskytkentä" | |||||||
| msgid "LDAP Source Property Mappings" | msgid "LDAP Source Property Mappings" | ||||||
| msgstr "LDAP-lähteen ominaisuuskytkennät" | 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 | #: authentik/sources/ldap/models.py | ||||||
| msgid "User LDAP Source Connection" | msgid "User LDAP Source Connection" | ||||||
| msgstr "" | msgstr "" | ||||||
| @ -2837,6 +2882,11 @@ msgstr "Ryhmän SAML-lähteen yhteys" | |||||||
| msgid "Group SAML Source Connections" | msgid "Group SAML Source Connections" | ||||||
| msgstr "Ryhmän SAML-lähteen yhteydet" | 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 | #: authentik/sources/scim/models.py | ||||||
| msgid "SCIM Source" | msgid "SCIM Source" | ||||||
| msgstr "SCIM-lähde" | msgstr "SCIM-lähde" | ||||||
| @ -3216,6 +3266,10 @@ msgstr "Käyttäjän hyväksyntä" | |||||||
| msgid "User Consents" | msgid "User Consents" | ||||||
| msgstr "Käyttäjän hyväksynnät" | 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 | #: authentik/stages/deny/models.py | ||||||
| msgid "Deny Stage" | msgid "Deny Stage" | ||||||
| msgstr "Kieltovaihe" | msgstr "Kieltovaihe" | ||||||
| @ -3232,6 +3286,14 @@ msgstr "Valevaihe" | |||||||
| msgid "Dummy Stages" | msgid "Dummy Stages" | ||||||
| msgstr "Valevaiheet" | 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 | #: authentik/stages/email/models.py | ||||||
| msgid "Password Reset" | msgid "Password Reset" | ||||||
| msgstr "Salasanan nollaus" | msgstr "Salasanan nollaus" | ||||||
|  | |||||||
| @ -19,7 +19,7 @@ msgid "" | |||||||
| msgstr "" | msgstr "" | ||||||
| "Project-Id-Version: PACKAGE VERSION\n" | "Project-Id-Version: PACKAGE VERSION\n" | ||||||
| "Report-Msgid-Bugs-To: \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" | "PO-Revision-Date: 2022-09-26 16:47+0000\n" | ||||||
| "Last-Translator: Marc Schmitt, 2025\n" | "Last-Translator: Marc Schmitt, 2025\n" | ||||||
| "Language-Team: French (https://app.transifex.com/authentik/teams/119923/fr/)\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" | msgstr "Démarrage d'une synchronisation complète du fournisseur" | ||||||
|  |  | ||||||
| #: authentik/lib/sync/outgoing/tasks.py | #: authentik/lib/sync/outgoing/tasks.py | ||||||
| #, python-brace-format | msgid "Syncing users" | ||||||
| msgid "Syncing page {page} of users" | msgstr "Synchronisation des utilisateurs" | ||||||
| msgstr "Synchronisation de la page {page} d'utilisateurs" |  | ||||||
|  | #: authentik/lib/sync/outgoing/tasks.py | ||||||
|  | msgid "Syncing groups" | ||||||
|  | msgstr "Synchronisation des groupes" | ||||||
|  |  | ||||||
| #: authentik/lib/sync/outgoing/tasks.py | #: authentik/lib/sync/outgoing/tasks.py | ||||||
| #, python-brace-format | #, python-brace-format | ||||||
| @ -2508,6 +2511,14 @@ msgstr "" | |||||||
| "plutôt que sur un attribut de groupe. Cela permet la résolution des groupes " | "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." | "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 | #: authentik/sources/ldap/models.py | ||||||
| msgid "LDAP Source" | msgid "LDAP Source" | ||||||
| msgstr "Source LDAP" | msgstr "Source LDAP" | ||||||
| @ -2524,6 +2535,13 @@ msgstr "Mappage de propriété source LDAP" | |||||||
| msgid "LDAP Source Property Mappings" | msgid "LDAP Source Property Mappings" | ||||||
| msgstr "Mappages de propriété source LDAP" | 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 | #: authentik/sources/ldap/models.py | ||||||
| msgid "User LDAP Source Connection" | msgid "User LDAP Source Connection" | ||||||
| msgstr "Connexion de l'utilisateur à la source LDAP" | msgstr "Connexion de l'utilisateur à la source LDAP" | ||||||
| @ -2918,6 +2936,11 @@ msgstr "Connexion du groupe à la source SAML" | |||||||
| msgid "Group SAML Source Connections" | msgid "Group SAML Source Connections" | ||||||
| msgstr "Connexions du groupe à la source SAML" | 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 | #: authentik/sources/scim/models.py | ||||||
| msgid "SCIM Source" | msgid "SCIM Source" | ||||||
| msgstr "Source SCIM" | msgstr "Source SCIM" | ||||||
| @ -3308,6 +3331,10 @@ msgstr "Consentement Utilisateur" | |||||||
| msgid "User Consents" | msgid "User Consents" | ||||||
| msgstr "Consentements Utilisateur" | 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 | #: authentik/stages/deny/models.py | ||||||
| msgid "Deny Stage" | msgid "Deny Stage" | ||||||
| msgstr "Étape de Refus" | msgstr "Étape de Refus" | ||||||
| @ -3324,6 +3351,14 @@ msgstr "Étape factice" | |||||||
| msgid "Dummy Stages" | msgid "Dummy Stages" | ||||||
| msgstr "Étapes factices" | 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 | #: authentik/stages/email/models.py | ||||||
| msgid "Password Reset" | msgid "Password Reset" | ||||||
| msgstr "Réinitialiser le Mot de Passe" | msgstr "Réinitialiser le Mot de Passe" | ||||||
|  | |||||||
| @ -20,7 +20,7 @@ msgid "" | |||||||
| msgstr "" | msgstr "" | ||||||
| "Project-Id-Version: PACKAGE VERSION\n" | "Project-Id-Version: PACKAGE VERSION\n" | ||||||
| "Report-Msgid-Bugs-To: \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" | "PO-Revision-Date: 2022-09-26 16:47+0000\n" | ||||||
| "Last-Translator: Kowalski Dragon (kowalski7cc) <kowalski.7cc@gmail.com>, 2025\n" | "Last-Translator: Kowalski Dragon (kowalski7cc) <kowalski.7cc@gmail.com>, 2025\n" | ||||||
| "Language-Team: Italian (https://app.transifex.com/authentik/teams/119923/it/)\n" | "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." | msgid "Web Certificate used by the authentik Core webserver." | ||||||
| msgstr "Certificato Web utilizzato dal server Web authentik Core." | msgstr "Certificato Web utilizzato dal server Web authentik Core." | ||||||
|  |  | ||||||
|  | #: authentik/brands/models.py | ||||||
|  | msgid "Certificates used for client authentication." | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: authentik/brands/models.py | #: authentik/brands/models.py | ||||||
| msgid "Brand" | msgid "Brand" | ||||||
| msgstr "Brand" | msgstr "Brand" | ||||||
| @ -672,6 +676,33 @@ msgstr "Dispositivi di Accesso" | |||||||
| msgid "Verifying your browser..." | msgid "Verifying your browser..." | ||||||
| msgstr "Verifica del tuo 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 | #: authentik/enterprise/stages/source/models.py | ||||||
| msgid "" | msgid "" | ||||||
| "Amount of time a user can take to return from the source to continue the " | "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" | msgstr "Avvio della sincronizzazione completa del provider" | ||||||
|  |  | ||||||
| #: authentik/lib/sync/outgoing/tasks.py | #: authentik/lib/sync/outgoing/tasks.py | ||||||
| #, python-brace-format | msgid "Syncing users" | ||||||
| msgid "Syncing page {page} of users" | msgstr "" | ||||||
| msgstr "Sincronizzando pagina {page} degli utenti" |  | ||||||
|  | #: authentik/lib/sync/outgoing/tasks.py | ||||||
|  | msgid "Syncing groups" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: authentik/lib/sync/outgoing/tasks.py | #: authentik/lib/sync/outgoing/tasks.py | ||||||
| #, python-brace-format | #, python-brace-format | ||||||
| @ -2463,6 +2497,12 @@ msgstr "" | |||||||
| "attributo di gruppo. Questo consente la risoluzione di gruppi nidificati su " | "attributo di gruppo. Questo consente la risoluzione di gruppi nidificati su " | ||||||
| "sistemi come FreeIPA e Active Directory." | "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 | #: authentik/sources/ldap/models.py | ||||||
| msgid "LDAP Source" | msgid "LDAP Source" | ||||||
| msgstr "Sorgente LDAP" | msgstr "Sorgente LDAP" | ||||||
| @ -2479,6 +2519,11 @@ msgstr "Mappatura delle proprietà sorgente LDAP" | |||||||
| msgid "LDAP Source Property Mappings" | msgid "LDAP Source Property Mappings" | ||||||
| msgstr "Mappature delle proprietà della sorgente LDAP" | 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 | #: authentik/sources/ldap/models.py | ||||||
| msgid "User LDAP Source Connection" | msgid "User LDAP Source Connection" | ||||||
| msgstr "Connessione Sorgente LDAP Utente" | msgstr "Connessione Sorgente LDAP Utente" | ||||||
| @ -2872,6 +2917,11 @@ msgstr "Connessione sorgente SAML di gruppo" | |||||||
| msgid "Group SAML Source Connections" | msgid "Group SAML Source Connections" | ||||||
| msgstr "Connessioni sorgente SAML di gruppo" | 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 | #: authentik/sources/scim/models.py | ||||||
| msgid "SCIM Source" | msgid "SCIM Source" | ||||||
| msgstr "Sorgente SCIM" | msgstr "Sorgente SCIM" | ||||||
| @ -3269,6 +3319,10 @@ msgstr "Consenso utente" | |||||||
| msgid "User Consents" | msgid "User Consents" | ||||||
| msgstr "Consensi utente" | msgstr "Consensi utente" | ||||||
|  |  | ||||||
|  | #: authentik/stages/consent/stage.py | ||||||
|  | msgid "Invalid consent token, re-showing prompt" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: authentik/stages/deny/models.py | #: authentik/stages/deny/models.py | ||||||
| msgid "Deny Stage" | msgid "Deny Stage" | ||||||
| msgstr "Fase di negazione" | msgstr "Fase di negazione" | ||||||
| @ -3285,6 +3339,14 @@ msgstr "Fase fittizia" | |||||||
| msgid "Dummy Stages" | msgid "Dummy Stages" | ||||||
| msgstr "Fasi fittizie" | 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 | #: authentik/stages/email/models.py | ||||||
| msgid "Password Reset" | msgid "Password Reset" | ||||||
| msgstr "Ripristino password" | msgstr "Ripristino password" | ||||||
|  | |||||||
| @ -12,7 +12,7 @@ msgid "" | |||||||
| msgstr "" | msgstr "" | ||||||
| "Project-Id-Version: PACKAGE VERSION\n" | "Project-Id-Version: PACKAGE VERSION\n" | ||||||
| "Report-Msgid-Bugs-To: \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" | "PO-Revision-Date: 2022-09-26 16:47+0000\n" | ||||||
| "Last-Translator: NavyStack, 2023\n" | "Last-Translator: NavyStack, 2023\n" | ||||||
| "Language-Team: Korean (https://app.transifex.com/authentik/teams/119923/ko/)\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." | msgid "Web Certificate used by the authentik Core webserver." | ||||||
| msgstr "Authentik Core 웹서버에서 사용하는 웹 인증서." | msgstr "Authentik Core 웹서버에서 사용하는 웹 인증서." | ||||||
|  |  | ||||||
|  | #: authentik/brands/models.py | ||||||
|  | msgid "Certificates used for client authentication." | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: authentik/brands/models.py | #: authentik/brands/models.py | ||||||
| msgid "Brand" | msgid "Brand" | ||||||
| msgstr "" | msgstr "" | ||||||
| @ -625,6 +629,33 @@ msgstr "" | |||||||
| msgid "Verifying your browser..." | msgid "Verifying your browser..." | ||||||
| msgstr "" | 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 | #: authentik/enterprise/stages/source/models.py | ||||||
| msgid "" | msgid "" | ||||||
| "Amount of time a user can take to return from the source to continue the " | "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 "" | msgstr "" | ||||||
|  |  | ||||||
| #: authentik/lib/sync/outgoing/tasks.py | #: authentik/lib/sync/outgoing/tasks.py | ||||||
| #, python-brace-format | msgid "Syncing users" | ||||||
| msgid "Syncing page {page} of users" | msgstr "" | ||||||
|  |  | ||||||
|  | #: authentik/lib/sync/outgoing/tasks.py | ||||||
|  | msgid "Syncing groups" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: authentik/lib/sync/outgoing/tasks.py | #: authentik/lib/sync/outgoing/tasks.py | ||||||
| @ -2263,6 +2297,12 @@ msgid "" | |||||||
| "Active Directory" | "Active Directory" | ||||||
| msgstr "" | 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 | #: authentik/sources/ldap/models.py | ||||||
| msgid "LDAP Source" | msgid "LDAP Source" | ||||||
| msgstr "LDAP 소스" | msgstr "LDAP 소스" | ||||||
| @ -2279,6 +2319,11 @@ msgstr "" | |||||||
| msgid "LDAP Source Property Mappings" | msgid "LDAP Source Property Mappings" | ||||||
| msgstr "" | 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 | #: authentik/sources/ldap/models.py | ||||||
| msgid "User LDAP Source Connection" | msgid "User LDAP Source Connection" | ||||||
| msgstr "" | msgstr "" | ||||||
| @ -2657,6 +2702,11 @@ msgstr "" | |||||||
| msgid "Group SAML Source Connections" | msgid "Group SAML Source Connections" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
|  | #: authentik/sources/saml/views.py | ||||||
|  | #, python-brace-format | ||||||
|  | msgid "Continue to {source_name}" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: authentik/sources/scim/models.py | #: authentik/sources/scim/models.py | ||||||
| msgid "SCIM Source" | msgid "SCIM Source" | ||||||
| msgstr "" | msgstr "" | ||||||
| @ -3017,6 +3067,10 @@ msgstr "사용자 동의" | |||||||
| msgid "User Consents" | msgid "User Consents" | ||||||
| msgstr "사용자 동의" | msgstr "사용자 동의" | ||||||
|  |  | ||||||
|  | #: authentik/stages/consent/stage.py | ||||||
|  | msgid "Invalid consent token, re-showing prompt" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: authentik/stages/deny/models.py | #: authentik/stages/deny/models.py | ||||||
| msgid "Deny Stage" | msgid "Deny Stage" | ||||||
| msgstr "거부 스테이지" | msgstr "거부 스테이지" | ||||||
| @ -3033,6 +3087,14 @@ msgstr "더미 스테이지" | |||||||
| msgid "Dummy Stages" | msgid "Dummy Stages" | ||||||
| msgstr "더미 스테이지" | 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 | #: authentik/stages/email/models.py | ||||||
| msgid "Password Reset" | msgid "Password Reset" | ||||||
| msgstr "비밀번호 초기화" | msgstr "비밀번호 초기화" | ||||||
|  | |||||||
										
											Binary file not shown.
										
									
								
							| @ -19,7 +19,7 @@ msgid "" | |||||||
| msgstr "" | msgstr "" | ||||||
| "Project-Id-Version: PACKAGE VERSION\n" | "Project-Id-Version: PACKAGE VERSION\n" | ||||||
| "Report-Msgid-Bugs-To: \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" | "PO-Revision-Date: 2022-09-26 16:47+0000\n" | ||||||
| "Last-Translator: Dany Sluijk, 2025\n" | "Last-Translator: Dany Sluijk, 2025\n" | ||||||
| "Language-Team: Dutch (https://app.transifex.com/authentik/teams/119923/nl/)\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." | msgid "Web Certificate used by the authentik Core webserver." | ||||||
| msgstr "Webcertificaat gebruikt door de 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 | #: authentik/brands/models.py | ||||||
| msgid "Brand" | msgid "Brand" | ||||||
| msgstr "Merk" | msgstr "Merk" | ||||||
| @ -191,6 +195,7 @@ msgid "User's display name." | |||||||
| msgstr "Weergavenaam van de gebruiker." | msgstr "Weergavenaam van de gebruiker." | ||||||
|  |  | ||||||
| #: authentik/core/models.py authentik/providers/oauth2/models.py | #: authentik/core/models.py authentik/providers/oauth2/models.py | ||||||
|  | #: authentik/rbac/models.py | ||||||
| msgid "User" | msgid "User" | ||||||
| msgstr "Gebruiker" | msgstr "Gebruiker" | ||||||
|  |  | ||||||
| @ -379,6 +384,18 @@ msgstr "Eigenschapskoppeling" | |||||||
| msgid "Property Mappings" | msgid "Property Mappings" | ||||||
| msgstr "Eigenschapskoppelingen" | 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 | #: authentik/core/models.py | ||||||
| msgid "Authenticated Session" | msgid "Authenticated Session" | ||||||
| msgstr "Geauthenticeerde Sessie" | msgstr "Geauthenticeerde Sessie" | ||||||
| @ -486,6 +503,38 @@ msgstr "Licentie Gebruik" | |||||||
| msgid "License Usage Records" | msgid "License Usage Records" | ||||||
| msgstr "Licentie Gebruik 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 | #: authentik/enterprise/policy.py | ||||||
| msgid "Enterprise required to access this feature." | msgid "Enterprise required to access this feature." | ||||||
| msgstr "Enterprise benodigd voor toegang tot deze functie." | msgstr "Enterprise benodigd voor toegang tot deze functie." | ||||||
| @ -622,6 +671,33 @@ msgstr "" | |||||||
| msgid "Verifying your browser..." | msgid "Verifying your browser..." | ||||||
| msgstr "Uw browser wordt geverifieerd..." | 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 | #: authentik/enterprise/stages/source/models.py | ||||||
| msgid "" | msgid "" | ||||||
| "Amount of time a user can take to return from the source to continue the " | "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 "" | msgstr "" | ||||||
|  |  | ||||||
| #: authentik/lib/sync/outgoing/tasks.py | #: authentik/lib/sync/outgoing/tasks.py | ||||||
| #, python-brace-format | msgid "Syncing users" | ||||||
| msgid "Syncing page {page} of users" | msgstr "" | ||||||
|  |  | ||||||
|  | #: authentik/lib/sync/outgoing/tasks.py | ||||||
|  | msgid "Syncing groups" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: authentik/lib/sync/outgoing/tasks.py | #: authentik/lib/sync/outgoing/tasks.py | ||||||
| @ -1265,12 +1344,6 @@ msgstr "" | |||||||
| msgid "Clear Policy's cache metrics" | msgid "Clear Policy's cache metrics" | ||||||
| msgstr "" | 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 | #: authentik/policies/password/models.py | ||||||
| msgid "How many times the password hash is allowed to be on haveibeenpwned" | msgid "How many times the password hash is allowed to be on haveibeenpwned" | ||||||
| msgstr "Hoe vaak het wachtwoordhash op haveibeenpwned mag voorkomen" | 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 " | "Als de zxcvbn-score gelijk is aan of lager is dan deze waarde, zal het " | ||||||
| "beleid falen." | "beleid falen." | ||||||
|  |  | ||||||
| #: authentik/policies/password/models.py |  | ||||||
| msgid "Password not set in context" |  | ||||||
| msgstr "Wachtwoord niet ingesteld in context" |  | ||||||
|  |  | ||||||
| #: authentik/policies/password/models.py | #: authentik/policies/password/models.py | ||||||
| msgid "Invalid password." | msgid "Invalid password." | ||||||
| msgstr "" | msgstr "" | ||||||
| @ -1327,20 +1396,6 @@ msgstr "Reputatie Score" | |||||||
| msgid "Reputation Scores" | msgid "Reputation Scores" | ||||||
| msgstr "Reputatie 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 | #: authentik/policies/templates/policies/denied.html | ||||||
| msgid "Permission denied" | msgid "Permission denied" | ||||||
| msgstr "Toestemming geweigerd" | msgstr "Toestemming geweigerd" | ||||||
| @ -2160,6 +2215,10 @@ msgstr "" | |||||||
| msgid "Roles" | msgid "Roles" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
|  | #: authentik/rbac/models.py | ||||||
|  | msgid "Initial Permissions" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: authentik/rbac/models.py | #: authentik/rbac/models.py | ||||||
| msgid "System permission" | msgid "System permission" | ||||||
| msgstr "" | msgstr "" | ||||||
| @ -2392,6 +2451,12 @@ msgid "" | |||||||
| "Active Directory" | "Active Directory" | ||||||
| msgstr "" | 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 | #: authentik/sources/ldap/models.py | ||||||
| msgid "LDAP Source" | msgid "LDAP Source" | ||||||
| msgstr "LDAP-bron" | msgstr "LDAP-bron" | ||||||
| @ -2408,6 +2473,27 @@ msgstr "" | |||||||
| msgid "LDAP Source Property Mappings" | msgid "LDAP Source Property Mappings" | ||||||
| msgstr "" | 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 | #: authentik/sources/ldap/signals.py | ||||||
| msgid "Password does not match Active Directory Complexity." | msgid "Password does not match Active Directory Complexity." | ||||||
| msgstr "" | msgstr "" | ||||||
| @ -2417,6 +2503,14 @@ msgstr "" | |||||||
| msgid "No token received." | msgid "No token received." | ||||||
| msgstr "Geen token ontvangen." | 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 | #: authentik/sources/oauth/models.py | ||||||
| msgid "Request Token URL" | msgid "Request Token URL" | ||||||
| msgstr "URL voor aanvragen van token" | msgstr "URL voor aanvragen van token" | ||||||
| @ -2458,6 +2552,12 @@ msgstr "" | |||||||
| msgid "Additional Scopes" | msgid "Additional Scopes" | ||||||
| msgstr "Aanvullende 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 | #: authentik/sources/oauth/models.py | ||||||
| msgid "OAuth Source" | msgid "OAuth Source" | ||||||
| msgstr "OAuth-bron" | msgstr "OAuth-bron" | ||||||
| @ -2769,6 +2869,11 @@ msgstr "" | |||||||
| msgid "Group SAML Source Connections" | msgid "Group SAML Source Connections" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
|  | #: authentik/sources/saml/views.py | ||||||
|  | #, python-brace-format | ||||||
|  | msgid "Continue to {source_name}" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: authentik/sources/scim/models.py | #: authentik/sources/scim/models.py | ||||||
| msgid "SCIM Source" | msgid "SCIM Source" | ||||||
| msgstr "" | msgstr "" | ||||||
| @ -3142,6 +3247,10 @@ msgstr "Gebruikerstoestemming" | |||||||
| msgid "User Consents" | msgid "User Consents" | ||||||
| msgstr "Gebruikersinstemmingen" | msgstr "Gebruikersinstemmingen" | ||||||
|  |  | ||||||
|  | #: authentik/stages/consent/stage.py | ||||||
|  | msgid "Invalid consent token, re-showing prompt" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: authentik/stages/deny/models.py | #: authentik/stages/deny/models.py | ||||||
| msgid "Deny Stage" | msgid "Deny Stage" | ||||||
| msgstr "Weigerfase" | msgstr "Weigerfase" | ||||||
| @ -3158,6 +3267,14 @@ msgstr "Dummystadium" | |||||||
| msgid "Dummy Stages" | msgid "Dummy Stages" | ||||||
| msgstr "Dummystadia" | 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 | #: authentik/stages/email/models.py | ||||||
| msgid "Password Reset" | msgid "Password Reset" | ||||||
| msgstr "Wachtwoordherstel" | msgstr "Wachtwoordherstel" | ||||||
| @ -3357,6 +3474,12 @@ msgstr "" | |||||||
| "Wanneer ingeschakeld, slaagt de stap en gaat verder wanneer ongeldige " | "Wanneer ingeschakeld, slaagt de stap en gaat verder wanneer ongeldige " | ||||||
| "gebruikersgegevens zijn ingevoerd." | "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 | #: authentik/stages/identification/models.py | ||||||
| msgid "Optional enrollment flow, which is linked at the bottom of the page." | msgid "Optional enrollment flow, which is linked at the bottom of the page." | ||||||
| msgstr "Optionele inschrijvingsflow, die onderaan de pagina is gekoppeld." | msgstr "Optionele inschrijvingsflow, die onderaan de pagina is gekoppeld." | ||||||
| @ -3742,6 +3865,14 @@ msgstr "" | |||||||
| "Gebeurtenissen worden verwijderd na deze duur. (Indeling: " | "Gebeurtenissen worden verwijderd na deze duur. (Indeling: " | ||||||
| "weken=3;dagen=2;uren=3;seconden=2)." | "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 | #: authentik/tenants/models.py | ||||||
| msgid "The option configures the footer links on the flow executor pages." | 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." | msgstr "De optie stelt de voettekst links in op de flow uitvoer pagina's." | ||||||
|  | |||||||
| @ -11,7 +11,7 @@ msgid "" | |||||||
| msgstr "" | msgstr "" | ||||||
| "Project-Id-Version: PACKAGE VERSION\n" | "Project-Id-Version: PACKAGE VERSION\n" | ||||||
| "Report-Msgid-Bugs-To: \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" | "PO-Revision-Date: 2022-09-26 16:47+0000\n" | ||||||
| "Last-Translator: Hugo Bicho, 2025\n" | "Last-Translator: Hugo Bicho, 2025\n" | ||||||
| "Language-Team: Portuguese (https://app.transifex.com/authentik/teams/119923/pt/)\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." | msgid "Web Certificate used by the authentik Core webserver." | ||||||
| msgstr "Certificado Web usado pelo servidor web authentik Core." | msgstr "Certificado Web usado pelo servidor web authentik Core." | ||||||
|  |  | ||||||
|  | #: authentik/brands/models.py | ||||||
|  | msgid "Certificates used for client authentication." | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: authentik/brands/models.py | #: authentik/brands/models.py | ||||||
| msgid "Brand" | msgid "Brand" | ||||||
| msgstr "Marca" | msgstr "Marca" | ||||||
| @ -662,6 +666,33 @@ msgstr "Dispositivos do ponto de ligação" | |||||||
| msgid "Verifying your browser..." | msgid "Verifying your browser..." | ||||||
| msgstr "A verificar o seu 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 | #: authentik/enterprise/stages/source/models.py | ||||||
| msgid "" | msgid "" | ||||||
| "Amount of time a user can take to return from the source to continue the " | "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" | msgstr "Iniciando a sincronização completa com o provedor" | ||||||
|  |  | ||||||
| #: authentik/lib/sync/outgoing/tasks.py | #: authentik/lib/sync/outgoing/tasks.py | ||||||
| #, python-brace-format | msgid "Syncing users" | ||||||
| msgid "Syncing page {page} of users" | msgstr "" | ||||||
| msgstr "A sincronizar a página {page} dos utilizadores" |  | ||||||
|  | #: authentik/lib/sync/outgoing/tasks.py | ||||||
|  | msgid "Syncing groups" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: authentik/lib/sync/outgoing/tasks.py | #: authentik/lib/sync/outgoing/tasks.py | ||||||
| #, python-brace-format | #, python-brace-format | ||||||
| @ -2456,6 +2490,12 @@ msgstr "" | |||||||
| " um atributo do grupo. Isto permite a resolução de grupos hierárquicos em " | " um atributo do grupo. Isto permite a resolução de grupos hierárquicos em " | ||||||
| "sistemas como o FreeIPA e Active Directory." | "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 | #: authentik/sources/ldap/models.py | ||||||
| msgid "LDAP Source" | msgid "LDAP Source" | ||||||
| msgstr "Fonte LDAP" | msgstr "Fonte LDAP" | ||||||
| @ -2472,6 +2512,11 @@ msgstr "Mapeamento de propriedades de fonte LDAP" | |||||||
| msgid "LDAP Source Property Mappings" | msgid "LDAP Source Property Mappings" | ||||||
| msgstr "Mapeamentos de propriedades de fonte LDAP" | 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 | #: authentik/sources/ldap/models.py | ||||||
| msgid "User LDAP Source Connection" | msgid "User LDAP Source Connection" | ||||||
| msgstr "Ligação à fonte LDAP de Utilizador" | msgstr "Ligação à fonte LDAP de Utilizador" | ||||||
| @ -2865,6 +2910,11 @@ msgstr "Ligação à fonte SAML de Grupo" | |||||||
| msgid "Group SAML Source Connections" | msgid "Group SAML Source Connections" | ||||||
| msgstr "Ligações à fonte SAML de Grupo" | 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 | #: authentik/sources/scim/models.py | ||||||
| msgid "SCIM Source" | msgid "SCIM Source" | ||||||
| msgstr "Fonte SCIM" | msgstr "Fonte SCIM" | ||||||
| @ -3255,6 +3305,10 @@ msgstr "Consentimento do Utilizador" | |||||||
| msgid "User Consents" | msgid "User Consents" | ||||||
| msgstr "Consentimentos do Utilizador" | msgstr "Consentimentos do Utilizador" | ||||||
|  |  | ||||||
|  | #: authentik/stages/consent/stage.py | ||||||
|  | msgid "Invalid consent token, re-showing prompt" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: authentik/stages/deny/models.py | #: authentik/stages/deny/models.py | ||||||
| msgid "Deny Stage" | msgid "Deny Stage" | ||||||
| msgstr "Etapa de negação" | msgstr "Etapa de negação" | ||||||
| @ -3271,6 +3325,14 @@ msgstr "Etapa fictícia" | |||||||
| msgid "Dummy Stages" | msgid "Dummy Stages" | ||||||
| msgstr "Etapas fictícias" | 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 | #: authentik/stages/email/models.py | ||||||
| msgid "Password Reset" | msgid "Password Reset" | ||||||
| msgstr "Redefinição de Palavra-Passe" | msgstr "Redefinição de Palavra-Passe" | ||||||
|  | |||||||
										
											Binary file not shown.
										
									
								
							| @ -8,19 +8,19 @@ | |||||||
| # Josenivaldo Benito Junior, 2023 | # Josenivaldo Benito Junior, 2023 | ||||||
| # Caio Lima, 2023 | # Caio Lima, 2023 | ||||||
| # Hacklab, 2023 | # Hacklab, 2023 | ||||||
| # Wagner Santos, 2024 |  | ||||||
| # Rafael Mundel, 2024 | # Rafael Mundel, 2024 | ||||||
| # Anderson Silva Andrade <anderson.asa89@gmail.com>, 2025 | # Anderson Silva Andrade <anderson.asa89@gmail.com>, 2025 | ||||||
| # Gil Poiares-Oliveira, 2025 | # Gil Poiares-Oliveira, 2025 | ||||||
|  | # Wagner Santos, 2025 | ||||||
| #  | #  | ||||||
| #, fuzzy | #, fuzzy | ||||||
| msgid "" | msgid "" | ||||||
| msgstr "" | msgstr "" | ||||||
| "Project-Id-Version: PACKAGE VERSION\n" | "Project-Id-Version: PACKAGE VERSION\n" | ||||||
| "Report-Msgid-Bugs-To: \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" | "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" | "Language-Team: Portuguese (Brazil) (https://app.transifex.com/authentik/teams/119923/pt_BR/)\n" | ||||||
| "MIME-Version: 1.0\n" | "MIME-Version: 1.0\n" | ||||||
| "Content-Type: text/plain; charset=UTF-8\n" | "Content-Type: text/plain; charset=UTF-8\n" | ||||||
| @ -112,6 +112,10 @@ msgstr "" | |||||||
| msgid "Web Certificate used by the authentik Core webserver." | msgid "Web Certificate used by the authentik Core webserver." | ||||||
| msgstr "Certificado da Web usado pelo servidor da web authentik Core." | 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 | #: authentik/brands/models.py | ||||||
| msgid "Brand" | msgid "Brand" | ||||||
| msgstr "Brand" | msgstr "Brand" | ||||||
| @ -271,11 +275,11 @@ msgstr "Aplicativos" | |||||||
|  |  | ||||||
| #: authentik/core/models.py | #: authentik/core/models.py | ||||||
| msgid "Application Entitlement" | msgid "Application Entitlement" | ||||||
| msgstr "" | msgstr "Autorização de aplicação" | ||||||
|  |  | ||||||
| #: authentik/core/models.py | #: authentik/core/models.py | ||||||
| msgid "Application Entitlements" | msgid "Application Entitlements" | ||||||
| msgstr "" | msgstr "Autorizações de aplicação" | ||||||
|  |  | ||||||
| #: authentik/core/models.py | #: authentik/core/models.py | ||||||
| msgid "Use the source-specific identifier" | msgid "Use the source-specific identifier" | ||||||
| @ -379,15 +383,15 @@ msgstr "Mapeamentos de propriedades" | |||||||
|  |  | ||||||
| #: authentik/core/models.py | #: authentik/core/models.py | ||||||
| msgid "session data" | msgid "session data" | ||||||
| msgstr "" | msgstr "dados de sessão" | ||||||
|  |  | ||||||
| #: authentik/core/models.py | #: authentik/core/models.py | ||||||
| msgid "Session" | msgid "Session" | ||||||
| msgstr "" | msgstr "Sessão" | ||||||
|  |  | ||||||
| #: authentik/core/models.py | #: authentik/core/models.py | ||||||
| msgid "Sessions" | msgid "Sessions" | ||||||
| msgstr "" | msgstr "Sessões" | ||||||
|  |  | ||||||
| #: authentik/core/models.py | #: authentik/core/models.py | ||||||
| msgid "Authenticated Session" | msgid "Authenticated Session" | ||||||
| @ -505,7 +509,7 @@ msgstr "" | |||||||
|  |  | ||||||
| #: authentik/enterprise/policies/unique_password/models.py | #: authentik/enterprise/policies/unique_password/models.py | ||||||
| msgid "Number of passwords to check against." | msgid "Number of passwords to check against." | ||||||
| msgstr "" | msgstr "Número de senhas para verificar." | ||||||
|  |  | ||||||
| #: authentik/enterprise/policies/unique_password/models.py | #: authentik/enterprise/policies/unique_password/models.py | ||||||
| #: authentik/policies/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 | #: authentik/enterprise/policies/unique_password/models.py | ||||||
| msgid "This password has been used previously. Please choose a different one." | 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 | #: authentik/enterprise/policies/unique_password/models.py | ||||||
| msgid "Password Uniqueness Policy" | msgid "Password Uniqueness Policy" | ||||||
| msgstr "" | msgstr "Política de exclusividade de senha" | ||||||
|  |  | ||||||
| #: authentik/enterprise/policies/unique_password/models.py | #: authentik/enterprise/policies/unique_password/models.py | ||||||
| msgid "Password Uniqueness Policies" | msgid "Password Uniqueness Policies" | ||||||
| msgstr "" | msgstr "Políticas de exclusividade de senha" | ||||||
|  |  | ||||||
| #: authentik/enterprise/policies/unique_password/models.py | #: authentik/enterprise/policies/unique_password/models.py | ||||||
| msgid "User Password History" | msgid "User Password History" | ||||||
| msgstr "" | msgstr "Histórico de senhas do usuário" | ||||||
|  |  | ||||||
| #: authentik/enterprise/policy.py | #: authentik/enterprise/policy.py | ||||||
| msgid "Enterprise required to access this feature." | msgid "Enterprise required to access this feature." | ||||||
| @ -610,39 +614,39 @@ msgstr "Chave de Assinatura" | |||||||
|  |  | ||||||
| #: authentik/enterprise/providers/ssf/models.py | #: authentik/enterprise/providers/ssf/models.py | ||||||
| msgid "Key used to sign the SSF Events." | msgid "Key used to sign the SSF Events." | ||||||
| msgstr "" | msgstr "Chave utilizada para assinar os eventos SSF." | ||||||
|  |  | ||||||
| #: authentik/enterprise/providers/ssf/models.py | #: authentik/enterprise/providers/ssf/models.py | ||||||
| msgid "Shared Signals Framework Provider" | msgid "Shared Signals Framework Provider" | ||||||
| msgstr "" | msgstr "Provedor de Shared Signals Framework" | ||||||
|  |  | ||||||
| #: authentik/enterprise/providers/ssf/models.py | #: authentik/enterprise/providers/ssf/models.py | ||||||
| msgid "Shared Signals Framework Providers" | msgid "Shared Signals Framework Providers" | ||||||
| msgstr "" | msgstr "Provedores de Shared Signals Framework" | ||||||
|  |  | ||||||
| #: authentik/enterprise/providers/ssf/models.py | #: authentik/enterprise/providers/ssf/models.py | ||||||
| msgid "Add stream to SSF provider" | msgid "Add stream to SSF provider" | ||||||
| msgstr "" | msgstr "Adicionar stream ao fornecedor SSF" | ||||||
|  |  | ||||||
| #: authentik/enterprise/providers/ssf/models.py | #: authentik/enterprise/providers/ssf/models.py | ||||||
| msgid "SSF Stream" | msgid "SSF Stream" | ||||||
| msgstr "" | msgstr "Stream SSF" | ||||||
|  |  | ||||||
| #: authentik/enterprise/providers/ssf/models.py | #: authentik/enterprise/providers/ssf/models.py | ||||||
| msgid "SSF Streams" | msgid "SSF Streams" | ||||||
| msgstr "" | msgstr "Streams SSF" | ||||||
|  |  | ||||||
| #: authentik/enterprise/providers/ssf/models.py | #: authentik/enterprise/providers/ssf/models.py | ||||||
| msgid "SSF Stream Event" | msgid "SSF Stream Event" | ||||||
| msgstr "" | msgstr "Evento de stream SSF" | ||||||
|  |  | ||||||
| #: authentik/enterprise/providers/ssf/models.py | #: authentik/enterprise/providers/ssf/models.py | ||||||
| msgid "SSF Stream Events" | msgid "SSF Stream Events" | ||||||
| msgstr "" | msgstr "Eventos de stream SSF" | ||||||
|  |  | ||||||
| #: authentik/enterprise/providers/ssf/tasks.py | #: authentik/enterprise/providers/ssf/tasks.py | ||||||
| msgid "Failed to send request" | msgid "Failed to send request" | ||||||
| msgstr "" | msgstr "Falha ao enviar requisição" | ||||||
|  |  | ||||||
| #: authentik/enterprise/stages/authenticator_endpoint_gdtc/models.py | #: authentik/enterprise/stages/authenticator_endpoint_gdtc/models.py | ||||||
| msgid "Endpoint Authenticator Google Device Trust Connector Stage" | msgid "Endpoint Authenticator Google Device Trust Connector Stage" | ||||||
| @ -664,6 +668,33 @@ msgstr "" | |||||||
| msgid "Verifying your browser..." | msgid "Verifying your browser..." | ||||||
| msgstr "" | 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 | #: authentik/enterprise/stages/source/models.py | ||||||
| msgid "" | msgid "" | ||||||
| "Amount of time a user can take to return from the source to continue the " | "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 | #: authentik/events/api/tasks.py | ||||||
| #, python-brace-format | #, python-brace-format | ||||||
| msgid "Successfully started task {name}." | msgid "Successfully started task {name}." | ||||||
| msgstr "" | msgstr "Tarefa {name} iniciada com sucesso." | ||||||
|  |  | ||||||
| #: authentik/events/models.py | #: authentik/events/models.py | ||||||
| msgid "Event" | msgid "Event" | ||||||
| @ -713,12 +744,16 @@ msgid "" | |||||||
| "Customize the body of the request. Mapping should return data that is JSON-" | "Customize the body of the request. Mapping should return data that is JSON-" | ||||||
| "serializable." | "serializable." | ||||||
| msgstr "" | msgstr "" | ||||||
|  | "Personalize o corpo do pedido. O mapeamento deve retornar dados que sejam " | ||||||
|  | "serializáveis em JSON." | ||||||
|  |  | ||||||
| #: authentik/events/models.py | #: authentik/events/models.py | ||||||
| msgid "" | msgid "" | ||||||
| "Configure additional headers to be sent. Mapping should return a dictionary " | "Configure additional headers to be sent. Mapping should return a dictionary " | ||||||
| "of key-value pairs" | "of key-value pairs" | ||||||
| msgstr "" | msgstr "" | ||||||
|  | "Configurar cabeçalhos adicionais a serem enviados. O mapeamento deve " | ||||||
|  | "retornar um dicionário de pares chave-valor" | ||||||
|  |  | ||||||
| #: authentik/events/models.py | #: authentik/events/models.py | ||||||
| msgid "" | msgid "" | ||||||
| @ -998,8 +1033,11 @@ msgid "Starting full provider sync" | |||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: authentik/lib/sync/outgoing/tasks.py | #: authentik/lib/sync/outgoing/tasks.py | ||||||
| #, python-brace-format | msgid "Syncing users" | ||||||
| msgid "Syncing page {page} of users" | msgstr "" | ||||||
|  |  | ||||||
|  | #: authentik/lib/sync/outgoing/tasks.py | ||||||
|  | msgid "Syncing groups" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: authentik/lib/sync/outgoing/tasks.py | #: authentik/lib/sync/outgoing/tasks.py | ||||||
| @ -1314,7 +1352,7 @@ msgstr "" | |||||||
| #: authentik/policies/password/models.py | #: authentik/policies/password/models.py | ||||||
| #, python-brace-format | #, python-brace-format | ||||||
| msgid "Password exists on {count} online lists." | 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 | #: authentik/policies/password/models.py | ||||||
| msgid "Password is too weak." | msgid "Password is too weak." | ||||||
| @ -2396,6 +2434,12 @@ msgid "" | |||||||
| "Active Directory" | "Active Directory" | ||||||
| msgstr "" | 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 | #: authentik/sources/ldap/models.py | ||||||
| msgid "LDAP Source" | msgid "LDAP Source" | ||||||
| msgstr "Fonte LDAP" | msgstr "Fonte LDAP" | ||||||
| @ -2412,6 +2456,11 @@ msgstr "" | |||||||
| msgid "LDAP Source Property Mappings" | msgid "LDAP Source Property Mappings" | ||||||
| msgstr "" | 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 | #: authentik/sources/ldap/models.py | ||||||
| msgid "User LDAP Source Connection" | msgid "User LDAP Source Connection" | ||||||
| msgstr "" | msgstr "" | ||||||
| @ -2802,6 +2851,11 @@ msgstr "" | |||||||
| msgid "Group SAML Source Connections" | msgid "Group SAML Source Connections" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
|  | #: authentik/sources/saml/views.py | ||||||
|  | #, python-brace-format | ||||||
|  | msgid "Continue to {source_name}" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: authentik/sources/scim/models.py | #: authentik/sources/scim/models.py | ||||||
| msgid "SCIM Source" | msgid "SCIM Source" | ||||||
| msgstr "" | msgstr "" | ||||||
| @ -3174,6 +3228,10 @@ msgstr "Consentimento do usuário" | |||||||
| msgid "User Consents" | msgid "User Consents" | ||||||
| msgstr "Consentimentos do usuário" | msgstr "Consentimentos do usuário" | ||||||
|  |  | ||||||
|  | #: authentik/stages/consent/stage.py | ||||||
|  | msgid "Invalid consent token, re-showing prompt" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: authentik/stages/deny/models.py | #: authentik/stages/deny/models.py | ||||||
| msgid "Deny Stage" | msgid "Deny Stage" | ||||||
| msgstr "Negar Estágio" | msgstr "Negar Estágio" | ||||||
| @ -3190,6 +3248,14 @@ msgstr "Palco fictício" | |||||||
| msgid "Dummy Stages" | msgid "Dummy Stages" | ||||||
| msgstr "Fases fictícias" | 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 | #: authentik/stages/email/models.py | ||||||
| msgid "Password Reset" | msgid "Password Reset" | ||||||
| msgstr "Redefinição de senha" | msgstr "Redefinição de senha" | ||||||
|  | |||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -18,7 +18,7 @@ msgid "" | |||||||
| msgstr "" | msgstr "" | ||||||
| "Project-Id-Version: PACKAGE VERSION\n" | "Project-Id-Version: PACKAGE VERSION\n" | ||||||
| "Report-Msgid-Bugs-To: \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" | "PO-Revision-Date: 2022-09-26 16:47+0000\n" | ||||||
| "Last-Translator: Marc Schmitt, 2025\n" | "Last-Translator: Marc Schmitt, 2025\n" | ||||||
| "Language-Team: Russian (https://app.transifex.com/authentik/teams/119923/ru/)\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." | msgid "Web Certificate used by the authentik Core webserver." | ||||||
| msgstr "Web Certificate используемый для authentik Core webserver." | msgstr "Web Certificate используемый для authentik Core webserver." | ||||||
|  |  | ||||||
|  | #: authentik/brands/models.py | ||||||
|  | msgid "Certificates used for client authentication." | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: authentik/brands/models.py | #: authentik/brands/models.py | ||||||
| msgid "Brand" | msgid "Brand" | ||||||
| msgstr "Бренд" | msgstr "Бренд" | ||||||
| @ -669,6 +673,33 @@ msgstr "Конечные устройства" | |||||||
| msgid "Verifying your browser..." | msgid "Verifying your browser..." | ||||||
| msgstr "Проверка вашего браузера..." | 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 | #: authentik/enterprise/stages/source/models.py | ||||||
| msgid "" | msgid "" | ||||||
| "Amount of time a user can take to return from the source to continue the " | "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 "Запуск полной синхронизации провайдера" | msgstr "Запуск полной синхронизации провайдера" | ||||||
|  |  | ||||||
| #: authentik/lib/sync/outgoing/tasks.py | #: authentik/lib/sync/outgoing/tasks.py | ||||||
| #, python-brace-format | msgid "Syncing users" | ||||||
| msgid "Syncing page {page} of users" | msgstr "" | ||||||
|  |  | ||||||
|  | #: authentik/lib/sync/outgoing/tasks.py | ||||||
|  | msgid "Syncing groups" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: authentik/lib/sync/outgoing/tasks.py | #: authentik/lib/sync/outgoing/tasks.py | ||||||
| @ -2430,6 +2464,12 @@ msgid "" | |||||||
| "Active Directory" | "Active Directory" | ||||||
| msgstr "" | 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 | #: authentik/sources/ldap/models.py | ||||||
| msgid "LDAP Source" | msgid "LDAP Source" | ||||||
| msgstr "Источник LDAP" | msgstr "Источник LDAP" | ||||||
| @ -2446,6 +2486,11 @@ msgstr "Сопоставление свойства LDAP источника" | |||||||
| msgid "LDAP Source Property Mappings" | msgid "LDAP Source Property Mappings" | ||||||
| msgstr "Сопоставление свойств LDAP источника" | 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 | #: authentik/sources/ldap/models.py | ||||||
| msgid "User LDAP Source Connection" | msgid "User LDAP Source Connection" | ||||||
| msgstr "" | msgstr "" | ||||||
| @ -2842,6 +2887,11 @@ msgstr "Групповое подключение к источнику SAML" | |||||||
| msgid "Group SAML Source Connections" | msgid "Group SAML Source Connections" | ||||||
| msgstr "Групповые подключения к источнику SAML" | msgstr "Групповые подключения к источнику SAML" | ||||||
|  |  | ||||||
|  | #: authentik/sources/saml/views.py | ||||||
|  | #, python-brace-format | ||||||
|  | msgid "Continue to {source_name}" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: authentik/sources/scim/models.py | #: authentik/sources/scim/models.py | ||||||
| msgid "SCIM Source" | msgid "SCIM Source" | ||||||
| msgstr "Источник SCIM" | msgstr "Источник SCIM" | ||||||
| @ -3219,6 +3269,10 @@ msgstr "Согласие пользователя" | |||||||
| msgid "User Consents" | msgid "User Consents" | ||||||
| msgstr "Согласия пользователя" | msgstr "Согласия пользователя" | ||||||
|  |  | ||||||
|  | #: authentik/stages/consent/stage.py | ||||||
|  | msgid "Invalid consent token, re-showing prompt" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: authentik/stages/deny/models.py | #: authentik/stages/deny/models.py | ||||||
| msgid "Deny Stage" | msgid "Deny Stage" | ||||||
| msgstr "Этап отказа" | msgstr "Этап отказа" | ||||||
| @ -3235,6 +3289,14 @@ msgstr "Фиктивный этап" | |||||||
| msgid "Dummy Stages" | msgid "Dummy Stages" | ||||||
| msgstr "Фиктивные этапы" | 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 | #: authentik/stages/email/models.py | ||||||
| msgid "Password Reset" | msgid "Password Reset" | ||||||
| msgstr "Сброс пароля" | msgstr "Сброс пароля" | ||||||
|  | |||||||
| @ -13,7 +13,7 @@ msgid "" | |||||||
| msgstr "" | msgstr "" | ||||||
| "Project-Id-Version: PACKAGE VERSION\n" | "Project-Id-Version: PACKAGE VERSION\n" | ||||||
| "Report-Msgid-Bugs-To: \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" | "PO-Revision-Date: 2022-09-26 16:47+0000\n" | ||||||
| "Last-Translator: Jens L. <jens@goauthentik.io>, 2025\n" | "Last-Translator: Jens L. <jens@goauthentik.io>, 2025\n" | ||||||
| "Language-Team: Turkish (https://app.transifex.com/authentik/teams/119923/tr/)\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." | msgid "Web Certificate used by the authentik Core webserver." | ||||||
| msgstr "Authentik Core web sunucusu tarafından kullanılan Web Sertifikası." | 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 | #: authentik/brands/models.py | ||||||
| msgid "Brand" | msgid "Brand" | ||||||
| msgstr "Marka" | msgstr "Marka" | ||||||
| @ -659,6 +663,33 @@ msgstr "Uç Nokta Cihazları" | |||||||
| msgid "Verifying your browser..." | msgid "Verifying your browser..." | ||||||
| msgstr "Tarayıcınız doğrulanıyor..." | 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 | #: authentik/enterprise/stages/source/models.py | ||||||
| msgid "" | msgid "" | ||||||
| "Amount of time a user can take to return from the source to continue the " | "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" | msgstr "Tam sağlayıcı senkronizasyonunu başlatma" | ||||||
|  |  | ||||||
| #: authentik/lib/sync/outgoing/tasks.py | #: authentik/lib/sync/outgoing/tasks.py | ||||||
| #, python-brace-format | msgid "Syncing users" | ||||||
| msgid "Syncing page {page} of users" | msgstr "" | ||||||
|  |  | ||||||
|  | #: authentik/lib/sync/outgoing/tasks.py | ||||||
|  | msgid "Syncing groups" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: authentik/lib/sync/outgoing/tasks.py | #: authentik/lib/sync/outgoing/tasks.py | ||||||
| @ -2430,6 +2464,12 @@ msgid "" | |||||||
| "Active Directory" | "Active Directory" | ||||||
| msgstr "" | 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 | #: authentik/sources/ldap/models.py | ||||||
| msgid "LDAP Source" | msgid "LDAP Source" | ||||||
| msgstr "LDAP Kaynağı" | msgstr "LDAP Kaynağı" | ||||||
| @ -2446,6 +2486,11 @@ msgstr "LDAP Kaynak Özellik Eşlemesi" | |||||||
| msgid "LDAP Source Property Mappings" | msgid "LDAP Source Property Mappings" | ||||||
| msgstr "LDAP Kaynak Özellik Eşlemeleri" | 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 | #: authentik/sources/ldap/models.py | ||||||
| msgid "User LDAP Source Connection" | msgid "User LDAP Source Connection" | ||||||
| msgstr "" | msgstr "" | ||||||
| @ -2837,6 +2882,11 @@ msgstr "Grup SAML Kaynak Bağlantısı" | |||||||
| msgid "Group SAML Source Connections" | msgid "Group SAML Source Connections" | ||||||
| msgstr "Grup SAML Kaynak Bağlantıları" | 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 | #: authentik/sources/scim/models.py | ||||||
| msgid "SCIM Source" | msgid "SCIM Source" | ||||||
| msgstr "SCIM Kaynak" | msgstr "SCIM Kaynak" | ||||||
| @ -3211,6 +3261,10 @@ msgstr "Kullanıcı Onayı" | |||||||
| msgid "User Consents" | msgid "User Consents" | ||||||
| msgstr "Kullanıcı Onayları" | msgstr "Kullanıcı Onayları" | ||||||
|  |  | ||||||
|  | #: authentik/stages/consent/stage.py | ||||||
|  | msgid "Invalid consent token, re-showing prompt" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: authentik/stages/deny/models.py | #: authentik/stages/deny/models.py | ||||||
| msgid "Deny Stage" | msgid "Deny Stage" | ||||||
| msgstr "Aşama Alanını Reddet" | msgstr "Aşama Alanını Reddet" | ||||||
| @ -3227,6 +3281,14 @@ msgstr "Kukla Aşaması" | |||||||
| msgid "Dummy Stages" | msgid "Dummy Stages" | ||||||
| msgstr "Kukla Aşamaları" | 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 | #: authentik/stages/email/models.py | ||||||
| msgid "Password Reset" | msgid "Password Reset" | ||||||
| msgstr "Parola Sıfırlama" | msgstr "Parola Sıfırlama" | ||||||
|  | |||||||
										
											Binary file not shown.
										
									
								
							| @ -15,7 +15,7 @@ msgid "" | |||||||
| msgstr "" | msgstr "" | ||||||
| "Project-Id-Version: PACKAGE VERSION\n" | "Project-Id-Version: PACKAGE VERSION\n" | ||||||
| "Report-Msgid-Bugs-To: \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" | "PO-Revision-Date: 2022-09-26 16:47+0000\n" | ||||||
| "Last-Translator: deluxghost, 2025\n" | "Last-Translator: deluxghost, 2025\n" | ||||||
| "Language-Team: Chinese Simplified (https://app.transifex.com/authentik/teams/119923/zh-Hans/)\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 "开始全量提供程序同步" | msgstr "开始全量提供程序同步" | ||||||
|  |  | ||||||
| #: authentik/lib/sync/outgoing/tasks.py | #: authentik/lib/sync/outgoing/tasks.py | ||||||
| #, python-brace-format | msgid "Syncing users" | ||||||
| msgid "Syncing page {page} of users" | msgstr "" | ||||||
| msgstr "正在同步用户页面 {page}" |  | ||||||
|  | #: authentik/lib/sync/outgoing/tasks.py | ||||||
|  | msgid "Syncing groups" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: authentik/lib/sync/outgoing/tasks.py | #: authentik/lib/sync/outgoing/tasks.py | ||||||
| #, python-brace-format | #, python-brace-format | ||||||
| @ -2285,6 +2288,12 @@ msgid "" | |||||||
| "Active Directory" | "Active Directory" | ||||||
| msgstr "基于用户属性而非组属性查询组成员身份。这允许在 FreeIPA 或 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 | #: authentik/sources/ldap/models.py | ||||||
| msgid "LDAP Source" | msgid "LDAP Source" | ||||||
| msgstr "LDAP 源" | msgstr "LDAP 源" | ||||||
| @ -2301,6 +2310,11 @@ msgstr "LDAP 源属性映射" | |||||||
| msgid "LDAP Source Property Mappings" | msgid "LDAP Source Property Mappings" | ||||||
| msgstr "LDAP 源属性映射" | 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 | #: authentik/sources/ldap/models.py | ||||||
| msgid "User LDAP Source Connection" | msgid "User LDAP Source Connection" | ||||||
| msgstr "用户 LDAP 源连接" | msgstr "用户 LDAP 源连接" | ||||||
| @ -2678,6 +2692,11 @@ msgstr "组 SAML 源连接" | |||||||
| msgid "Group SAML Source Connections" | msgid "Group SAML Source Connections" | ||||||
| msgstr "组 SAML 源连接" | msgstr "组 SAML 源连接" | ||||||
|  |  | ||||||
|  | #: authentik/sources/saml/views.py | ||||||
|  | #, python-brace-format | ||||||
|  | msgid "Continue to {source_name}" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: authentik/sources/scim/models.py | #: authentik/sources/scim/models.py | ||||||
| msgid "SCIM Source" | msgid "SCIM Source" | ||||||
| msgstr "SCIM 源" | msgstr "SCIM 源" | ||||||
| @ -3044,6 +3063,10 @@ msgstr "用户同意授权" | |||||||
| msgid "User Consents" | msgid "User Consents" | ||||||
| msgstr "用户同意授权" | msgstr "用户同意授权" | ||||||
|  |  | ||||||
|  | #: authentik/stages/consent/stage.py | ||||||
|  | msgid "Invalid consent token, re-showing prompt" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: authentik/stages/deny/models.py | #: authentik/stages/deny/models.py | ||||||
| msgid "Deny Stage" | msgid "Deny Stage" | ||||||
| msgstr "拒绝阶段" | msgstr "拒绝阶段" | ||||||
| @ -3060,6 +3083,14 @@ msgstr "虚拟阶段" | |||||||
| msgid "Dummy Stages" | msgid "Dummy Stages" | ||||||
| msgstr "虚拟阶段" | 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 | #: authentik/stages/email/models.py | ||||||
| msgid "Password Reset" | msgid "Password Reset" | ||||||
| msgstr "密码重置" | msgstr "密码重置" | ||||||
|  | |||||||
										
											Binary file not shown.
										
									
								
							| @ -14,7 +14,7 @@ msgid "" | |||||||
| msgstr "" | msgstr "" | ||||||
| "Project-Id-Version: PACKAGE VERSION\n" | "Project-Id-Version: PACKAGE VERSION\n" | ||||||
| "Report-Msgid-Bugs-To: \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" | "PO-Revision-Date: 2022-09-26 16:47+0000\n" | ||||||
| "Last-Translator: deluxghost, 2025\n" | "Last-Translator: deluxghost, 2025\n" | ||||||
| "Language-Team: Chinese (China) (https://app.transifex.com/authentik/teams/119923/zh_CN/)\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 "开始全量提供程序同步" | msgstr "开始全量提供程序同步" | ||||||
|  |  | ||||||
| #: authentik/lib/sync/outgoing/tasks.py | #: authentik/lib/sync/outgoing/tasks.py | ||||||
| #, python-brace-format | msgid "Syncing users" | ||||||
| msgid "Syncing page {page} of users" | msgstr "正在同步用户" | ||||||
| msgstr "正在同步用户页面 {page}" |  | ||||||
|  | #: authentik/lib/sync/outgoing/tasks.py | ||||||
|  | msgid "Syncing groups" | ||||||
|  | msgstr "正在同步组" | ||||||
|  |  | ||||||
| #: authentik/lib/sync/outgoing/tasks.py | #: authentik/lib/sync/outgoing/tasks.py | ||||||
| #, python-brace-format | #, python-brace-format | ||||||
| @ -2284,6 +2287,12 @@ msgid "" | |||||||
| "Active Directory" | "Active Directory" | ||||||
| msgstr "基于用户属性而非组属性查询组成员身份。这允许在 FreeIPA 或 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 | #: authentik/sources/ldap/models.py | ||||||
| msgid "LDAP Source" | msgid "LDAP Source" | ||||||
| msgstr "LDAP 源" | msgstr "LDAP 源" | ||||||
| @ -2300,6 +2309,11 @@ msgstr "LDAP 源属性映射" | |||||||
| msgid "LDAP Source Property Mappings" | msgid "LDAP Source Property Mappings" | ||||||
| msgstr "LDAP 源属性映射" | 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 | #: authentik/sources/ldap/models.py | ||||||
| msgid "User LDAP Source Connection" | msgid "User LDAP Source Connection" | ||||||
| msgstr "用户 LDAP 源连接" | msgstr "用户 LDAP 源连接" | ||||||
| @ -2677,6 +2691,11 @@ msgstr "组 SAML 源连接" | |||||||
| msgid "Group SAML Source Connections" | msgid "Group SAML Source Connections" | ||||||
| msgstr "组 SAML 源连接" | msgstr "组 SAML 源连接" | ||||||
|  |  | ||||||
|  | #: authentik/sources/saml/views.py | ||||||
|  | #, python-brace-format | ||||||
|  | msgid "Continue to {source_name}" | ||||||
|  | msgstr "继续前往 {source_name}" | ||||||
|  |  | ||||||
| #: authentik/sources/scim/models.py | #: authentik/sources/scim/models.py | ||||||
| msgid "SCIM Source" | msgid "SCIM Source" | ||||||
| msgstr "SCIM 源" | msgstr "SCIM 源" | ||||||
| @ -3043,6 +3062,10 @@ msgstr "用户同意授权" | |||||||
| msgid "User Consents" | msgid "User Consents" | ||||||
| msgstr "用户同意授权" | msgstr "用户同意授权" | ||||||
|  |  | ||||||
|  | #: authentik/stages/consent/stage.py | ||||||
|  | msgid "Invalid consent token, re-showing prompt" | ||||||
|  | msgstr "无效的同意令牌,将重新显示输入" | ||||||
|  |  | ||||||
| #: authentik/stages/deny/models.py | #: authentik/stages/deny/models.py | ||||||
| msgid "Deny Stage" | msgid "Deny Stage" | ||||||
| msgstr "拒绝阶段" | msgstr "拒绝阶段" | ||||||
| @ -3059,6 +3082,14 @@ msgstr "虚拟阶段" | |||||||
| msgid "Dummy Stages" | msgid "Dummy Stages" | ||||||
| msgstr "虚拟阶段" | 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 | #: authentik/stages/email/models.py | ||||||
| msgid "Password Reset" | msgid "Password Reset" | ||||||
| msgstr "密码重置" | msgstr "密码重置" | ||||||
|  | |||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user
	