Compare commits

..

49 Commits

Author SHA1 Message Date
Jens Langhammer
1db6104bef flows: return correct status code on error
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-12-18 13:23:24 +01:00
Jens L.
62dc04a684 stages: remove more global state (#18641)
* add auth for active flow

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* migrate duo

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* migrate sms

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* migrate consent

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* migrate email and fix broken tests

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fallback

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* merge flow plan when restoring from token

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-12-18 13:12:35 +01:00
dependabot[bot]
68f3bf6ec1 web: bump the storybook group across 1 directory with 5 updates (#18817)
Bumps the storybook group with 4 updates in the /web directory: [@storybook/addon-docs](https://github.com/storybookjs/storybook/tree/HEAD/code/addons/docs), [@storybook/addon-links](https://github.com/storybookjs/storybook/tree/HEAD/code/addons/links), [@storybook/web-components](https://github.com/storybookjs/storybook/tree/HEAD/code/renderers/web-components) and [@storybook/web-components-vite](https://github.com/storybookjs/storybook/tree/HEAD/code/frameworks/web-components-vite).


Updates `@storybook/addon-docs` from 10.1.7 to 10.1.8
- [Release notes](https://github.com/storybookjs/storybook/releases)
- [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md)
- [Commits](https://github.com/storybookjs/storybook/commits/v10.1.8/code/addons/docs)

Updates `@storybook/addon-links` from 10.1.7 to 10.1.8
- [Release notes](https://github.com/storybookjs/storybook/releases)
- [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md)
- [Commits](https://github.com/storybookjs/storybook/commits/v10.1.8/code/addons/links)

Updates `@storybook/web-components` from 10.1.7 to 10.1.8
- [Release notes](https://github.com/storybookjs/storybook/releases)
- [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md)
- [Commits](https://github.com/storybookjs/storybook/commits/v10.1.8/code/renderers/web-components)

Updates `@storybook/web-components-vite` from 10.1.7 to 10.1.8
- [Release notes](https://github.com/storybookjs/storybook/releases)
- [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md)
- [Commits](https://github.com/storybookjs/storybook/commits/v10.1.8/code/frameworks/web-components-vite)

Updates `storybook` from 10.1.7 to 10.1.8
- [Release notes](https://github.com/storybookjs/storybook/releases)
- [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md)
- [Commits](https://github.com/storybookjs/storybook/commits/v10.1.8/code/core)

---
updated-dependencies:
- dependency-name: "@storybook/addon-docs"
  dependency-version: 10.1.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: storybook
- dependency-name: "@storybook/addon-links"
  dependency-version: 10.1.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: storybook
- dependency-name: "@storybook/web-components"
  dependency-version: 10.1.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: storybook
- dependency-name: "@storybook/web-components-vite"
  dependency-version: 10.1.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: storybook
- dependency-name: storybook
  dependency-version: 10.1.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: storybook
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-18 13:02:01 +01:00
dependabot[bot]
8234613b76 web: bump the swc group across 1 directory with 11 updates (#18923)
Bumps the swc group with 1 update in the /web directory: [@swc/core](https://github.com/swc-project/swc).


Updates `@swc/core` from 1.15.5 to 1.15.6
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.15.5...v1.15.6)

Updates `@swc/core-darwin-arm64` from 1.15.5 to 1.15.6
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.15.5...v1.15.6)

Updates `@swc/core-darwin-x64` from 1.15.5 to 1.15.6
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.15.5...v1.15.6)

Updates `@swc/core-linux-arm-gnueabihf` from 1.15.5 to 1.15.6
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.15.5...v1.15.6)

Updates `@swc/core-linux-arm64-gnu` from 1.15.5 to 1.15.6
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.15.5...v1.15.6)

Updates `@swc/core-linux-arm64-musl` from 1.15.5 to 1.15.6
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.15.5...v1.15.6)

Updates `@swc/core-linux-x64-gnu` from 1.15.5 to 1.15.6
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.15.5...v1.15.6)

Updates `@swc/core-linux-x64-musl` from 1.15.5 to 1.15.6
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.15.5...v1.15.6)

Updates `@swc/core-win32-arm64-msvc` from 1.15.5 to 1.15.6
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.15.5...v1.15.6)

Updates `@swc/core-win32-ia32-msvc` from 1.15.5 to 1.15.6
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.15.5...v1.15.6)

Updates `@swc/core-win32-x64-msvc` from 1.15.5 to 1.15.6
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.15.5...v1.15.6)

---
updated-dependencies:
- dependency-name: "@swc/core"
  dependency-version: 1.15.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-darwin-arm64"
  dependency-version: 1.15.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-darwin-x64"
  dependency-version: 1.15.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-linux-arm-gnueabihf"
  dependency-version: 1.15.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-linux-arm64-gnu"
  dependency-version: 1.15.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-linux-arm64-musl"
  dependency-version: 1.15.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-linux-x64-gnu"
  dependency-version: 1.15.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-linux-x64-musl"
  dependency-version: 1.15.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-win32-arm64-msvc"
  dependency-version: 1.15.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-win32-ia32-msvc"
  dependency-version: 1.15.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-win32-x64-msvc"
  dependency-version: 1.15.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swc
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-18 13:01:49 +01:00
dependabot[bot]
eec998cc8d lifecycle/aws: bump aws-cdk from 2.1100.0 to 2.1100.1 in /lifecycle/aws (#18922)
Bumps [aws-cdk](https://github.com/aws/aws-cdk-cli/tree/HEAD/packages/aws-cdk) from 2.1100.0 to 2.1100.1.
- [Release notes](https://github.com/aws/aws-cdk-cli/releases)
- [Commits](https://github.com/aws/aws-cdk-cli/commits/aws-cdk@v2.1100.1/packages/aws-cdk)

---
updated-dependencies:
- dependency-name: aws-cdk
  dependency-version: 2.1100.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-18 13:00:30 +01:00
authentik-automation[bot]
d01aa6bebf core, web: update translations (#18920)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
2025-12-18 12:57:02 +01:00
dependabot[bot]
cbbf315662 web: bump knip from 5.74.0 to 5.75.1 in /web (#18924)
Bumps [knip](https://github.com/webpro-nl/knip/tree/HEAD/packages/knip) from 5.74.0 to 5.75.1.
- [Release notes](https://github.com/webpro-nl/knip/releases)
- [Commits](https://github.com/webpro-nl/knip/commits/5.75.1/packages/knip)

---
updated-dependencies:
- dependency-name: knip
  dependency-version: 5.75.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-18 12:55:58 +01:00
dependabot[bot]
45ca767fd8 web: bump chromedriver from 143.0.1 to 143.0.2 in /web (#18926)
Bumps [chromedriver](https://github.com/giggio/node-chromedriver) from 143.0.1 to 143.0.2.
- [Commits](https://github.com/giggio/node-chromedriver/compare/143.0.1...143.0.2)

---
updated-dependencies:
- dependency-name: chromedriver
  dependency-version: 143.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-18 12:55:32 +01:00
dependabot[bot]
5d3e2e89e0 core: bump goauthentik/fips-debian from 189345a to 10dadf1 (#18927)
Bumps goauthentik/fips-debian from `189345a` to `10dadf1`.

---
updated-dependencies:
- dependency-name: goauthentik/fips-debian
  dependency-version: trixie-slim-fips
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-18 12:55:06 +01:00
Marcelo Elizeche Landó
5e2f261a0c tests/e2e: handle StaleElementReferenceException in parse_json_content (#18842)
Add retry logic and safer text recover
2025-12-17 20:27:03 -03:00
Tana M Berry
10a421e678 web/admin: add UI copy to RBAC modal (#18917)
add UI copy to RBAC modal
2025-12-17 20:23:20 +00:00
Marc 'risson' Schmitt
668ad3dadf root: fix docker-compose data mount (#18903) 2025-12-17 16:33:50 -03:00
João C. Fernandes
e7903d5391 core/groups: optimize prefetch queries to fetch only required fields (#18448)
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2025-12-17 17:15:31 +00:00
Jens L.
e38fffc44c web/admin: reword some things on the device view page (#18785)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-12-17 17:33:36 +01:00
Marc 'risson' Schmitt
4bc2bca448 website/docs: 2025.12: remove superfluous changes (#18910) 2025-12-17 16:24:12 +00:00
Jens L.
48916303d8 api: fix latest version for public schema (#18902) 2025-12-17 15:12:14 +00:00
Marc 'risson' Schmitt
d28109da6a ci/release-tag: checkout correct branch for make test-docker (#18880) 2025-12-17 15:08:05 +00:00
Marc 'risson' Schmitt
3bd299d52a api: fix page_size with invalid query param (#18879) 2025-12-17 15:06:22 +00:00
Tana M Berry
57418582c5 website/docs: added list of Int Guide contributors (also edited frontmatter) (#18888)
* alphabetize

* added frontmatter

* Apply suggestions from code review

Signed-off-by: Dominic R <dominic@sdko.org>

---------

Signed-off-by: Dominic R <dominic@sdko.org>
Co-authored-by: Dominic R <dominic@sdko.org>
2025-12-17 09:01:05 -06:00
Marc 'risson' Schmitt
f37958bcd0 tasks/middleware: close connections on worker status update database error (#18881) 2025-12-17 15:45:37 +01:00
dependabot[bot]
8931b621b4 core: bump goauthentik.io/api/v3 from 3.2026020.1 to 3.2026020.3 (#18892)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-17 13:24:57 +00:00
dependabot[bot]
9d3d96bab1 web: bump the eslint group across 1 directory with 5 updates (#18851)
Bumps the eslint group with 5 updates in the /web directory:

| Package | From | To |
| --- | --- | --- |
| [@eslint/js](https://github.com/eslint/eslint/tree/HEAD/packages/js) | `9.39.1` | `9.39.2` |
| [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) | `8.49.0` | `8.50.0` |
| [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) | `8.49.0` | `8.50.0` |
| [eslint](https://github.com/eslint/eslint) | `9.39.1` | `9.39.2` |
| [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.49.0` | `8.50.0` |



Updates `@eslint/js` from 9.39.1 to 9.39.2
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/commits/v9.39.2/packages/js)

Updates `@typescript-eslint/eslint-plugin` from 8.49.0 to 8.50.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.50.0/packages/eslint-plugin)

Updates `@typescript-eslint/parser` from 8.49.0 to 8.50.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.50.0/packages/parser)

Updates `eslint` from 9.39.1 to 9.39.2
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v9.39.1...v9.39.2)

Updates `typescript-eslint` from 8.49.0 to 8.50.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.50.0/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: "@eslint/js"
  dependency-version: 9.39.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: eslint
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-version: 8.50.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: eslint
- dependency-name: "@typescript-eslint/parser"
  dependency-version: 8.50.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: eslint
- dependency-name: eslint
  dependency-version: 9.39.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: eslint
- dependency-name: typescript-eslint
  dependency-version: 8.50.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: eslint
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-17 13:15:56 +01:00
dependabot[bot]
712f0ed95e web: bump the swc group across 1 directory with 11 updates (#18818)
Bumps the swc group with 1 update in the /web directory: [@swc/core](https://github.com/swc-project/swc).


Updates `@swc/core` from 1.15.3 to 1.15.4
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.15.3...v1.15.4)

Updates `@swc/core-darwin-arm64` from 1.15.3 to 1.15.4
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.15.3...v1.15.4)

Updates `@swc/core-darwin-x64` from 1.15.3 to 1.15.4
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.15.3...v1.15.4)

Updates `@swc/core-linux-arm-gnueabihf` from 1.15.3 to 1.15.4
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.15.3...v1.15.4)

Updates `@swc/core-linux-arm64-gnu` from 1.15.3 to 1.15.4
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.15.3...v1.15.4)

Updates `@swc/core-linux-arm64-musl` from 1.15.3 to 1.15.4
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.15.3...v1.15.4)

Updates `@swc/core-linux-x64-gnu` from 1.15.3 to 1.15.4
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.15.3...v1.15.4)

Updates `@swc/core-linux-x64-musl` from 1.15.3 to 1.15.4
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.15.3...v1.15.4)

Updates `@swc/core-win32-arm64-msvc` from 1.15.3 to 1.15.4
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.15.3...v1.15.4)

Updates `@swc/core-win32-ia32-msvc` from 1.15.3 to 1.15.4
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.15.3...v1.15.4)

Updates `@swc/core-win32-x64-msvc` from 1.15.3 to 1.15.4
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.15.3...v1.15.4)

---
updated-dependencies:
- dependency-name: "@swc/core"
  dependency-version: 1.15.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-darwin-arm64"
  dependency-version: 1.15.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-darwin-x64"
  dependency-version: 1.15.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-linux-arm-gnueabihf"
  dependency-version: 1.15.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-linux-arm64-gnu"
  dependency-version: 1.15.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-linux-arm64-musl"
  dependency-version: 1.15.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-linux-x64-gnu"
  dependency-version: 1.15.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-linux-x64-musl"
  dependency-version: 1.15.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-win32-arm64-msvc"
  dependency-version: 1.15.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-win32-ia32-msvc"
  dependency-version: 1.15.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-win32-x64-msvc"
  dependency-version: 1.15.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swc
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-17 12:32:37 +01:00
dependabot[bot]
1cd9c7bf9d web: bump the goauthentik group across 1 directory with 3 updates (#18819)
Bumps the goauthentik group with 3 updates in the /web directory: [@goauthentik/esbuild-plugin-live-reload](https://github.com/goauthentik/authentik/tree/HEAD/packages/esbuild-plugin-live-reload), [@goauthentik/eslint-config](https://github.com/goauthentik/authentik/tree/HEAD/packages/eslint-config) and [@goauthentik/prettier-config](https://github.com/goauthentik/authentik/tree/HEAD/packages/prettier-config).


Updates `@goauthentik/esbuild-plugin-live-reload` from 1.3.1 to 1.4.0
- [Release notes](https://github.com/goauthentik/authentik/releases)
- [Commits](https://github.com/goauthentik/authentik/commits/HEAD/packages/esbuild-plugin-live-reload)

Updates `@goauthentik/eslint-config` from 1.1.1 to 1.2.0
- [Release notes](https://github.com/goauthentik/authentik/releases)
- [Commits](https://github.com/goauthentik/authentik/commits/HEAD/packages/eslint-config)

Updates `@goauthentik/prettier-config` from 3.2.1 to 3.3.1
- [Release notes](https://github.com/goauthentik/authentik/releases)
- [Commits](https://github.com/goauthentik/authentik/commits/HEAD/packages/prettier-config)

---
updated-dependencies:
- dependency-name: "@goauthentik/esbuild-plugin-live-reload"
  dependency-version: 1.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: goauthentik
- dependency-name: "@goauthentik/eslint-config"
  dependency-version: 1.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: goauthentik
- dependency-name: "@goauthentik/prettier-config"
  dependency-version: 3.3.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: goauthentik
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-17 12:32:22 +01:00
dependabot[bot]
fb23751079 lifecycle/aws: bump aws-cdk from 2.1034.0 to 2.1100.0 in /lifecycle/aws (#18850)
Bumps [aws-cdk](https://github.com/aws/aws-cdk-cli/tree/HEAD/packages/aws-cdk) from 2.1034.0 to 2.1100.0.
- [Release notes](https://github.com/aws/aws-cdk-cli/releases)
- [Commits](https://github.com/aws/aws-cdk-cli/commits/aws-cdk@v2.1100.0/packages/aws-cdk)

---
updated-dependencies:
- dependency-name: aws-cdk
  dependency-version: 2.1100.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-17 12:32:01 +01:00
dependabot[bot]
e49aace000 web: bump @sentry/browser from 10.30.0 to 10.31.0 in /web in the sentry group across 1 directory (#18893)
web: bump @sentry/browser in /web in the sentry group across 1 directory

Bumps the sentry group with 1 update in the /web directory: [@sentry/browser](https://github.com/getsentry/sentry-javascript).


Updates `@sentry/browser` from 10.30.0 to 10.31.0
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/10.30.0...10.31.0)

---
updated-dependencies:
- dependency-name: "@sentry/browser"
  dependency-version: 10.31.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: sentry
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-17 12:31:27 +01:00
dependabot[bot]
876b299f30 web: bump the bundler group across 1 directory with 7 updates (#18894)
Bumps the bundler group with 2 updates in the /web directory: [@vitest/browser](https://github.com/vitest-dev/vitest/tree/HEAD/packages/browser) and [esbuild](https://github.com/evanw/esbuild).


Updates `@vitest/browser` from 4.0.15 to 4.0.16
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.0.16/packages/browser)

Updates `@vitest/browser-playwright` from 4.0.15 to 4.0.16
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.0.16/packages/browser-playwright)

Updates `esbuild` from 0.27.1 to 0.27.2
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.27.1...v0.27.2)

Updates `vitest` from 4.0.15 to 4.0.16
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.0.16/packages/vitest)

Updates `@esbuild/darwin-arm64` from 0.27.1 to 0.27.2
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.27.1...v0.27.2)

Updates `@esbuild/linux-arm64` from 0.27.1 to 0.27.2
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.27.1...v0.27.2)

Updates `@esbuild/linux-x64` from 0.27.1 to 0.27.2
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.27.1...v0.27.2)

---
updated-dependencies:
- dependency-name: "@vitest/browser"
  dependency-version: 4.0.16
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: bundler
- dependency-name: "@vitest/browser-playwright"
  dependency-version: 4.0.16
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: bundler
- dependency-name: esbuild
  dependency-version: 0.27.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: bundler
- dependency-name: vitest
  dependency-version: 4.0.16
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: bundler
- dependency-name: "@esbuild/darwin-arm64"
  dependency-version: 0.27.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: bundler
- dependency-name: "@esbuild/linux-arm64"
  dependency-version: 0.27.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: bundler
- dependency-name: "@esbuild/linux-x64"
  dependency-version: 0.27.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: bundler
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-17 12:31:15 +01:00
dependabot[bot]
458439c396 web: bump the rollup group across 1 directory with 4 updates (#18852)
Bumps the rollup group with 4 updates in the /web directory: [@rollup/rollup-darwin-arm64](https://github.com/rollup/rollup), [@rollup/rollup-linux-arm64-gnu](https://github.com/rollup/rollup), [@rollup/rollup-linux-x64-gnu](https://github.com/rollup/rollup) and [rollup](https://github.com/rollup/rollup).


Updates `@rollup/rollup-darwin-arm64` from 4.53.3 to 4.53.4
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.53.3...v4.53.4)

Updates `@rollup/rollup-linux-arm64-gnu` from 4.53.3 to 4.53.4
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.53.3...v4.53.4)

Updates `@rollup/rollup-linux-x64-gnu` from 4.53.3 to 4.53.4
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.53.3...v4.53.4)

Updates `rollup` from 4.53.3 to 4.53.4
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.53.3...v4.53.4)

---
updated-dependencies:
- dependency-name: "@rollup/rollup-darwin-arm64"
  dependency-version: 4.53.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rollup
- dependency-name: "@rollup/rollup-linux-arm64-gnu"
  dependency-version: 4.53.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rollup
- dependency-name: "@rollup/rollup-linux-x64-gnu"
  dependency-version: 4.53.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rollup
- dependency-name: rollup
  dependency-version: 4.53.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rollup
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-17 12:29:44 +01:00
dependabot[bot]
d3d0effe9d web: bump @types/node from 25.0.0 to 25.0.3 in /web (#18895)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 25.0.0 to 25.0.3.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 25.0.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-17 12:29:30 +01:00
dependabot[bot]
413b073191 web: bump knip from 5.73.3 to 5.74.0 in /web (#18896)
Bumps [knip](https://github.com/webpro-nl/knip/tree/HEAD/packages/knip) from 5.73.3 to 5.74.0.
- [Release notes](https://github.com/webpro-nl/knip/releases)
- [Commits](https://github.com/webpro-nl/knip/commits/5.74.0/packages/knip)

---
updated-dependencies:
- dependency-name: knip
  dependency-version: 5.74.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-17 12:28:48 +01:00
dependabot[bot]
46747ae3f2 core: bump goauthentik/fips-debian from 2f19fc1 to 189345a (#18897)
Bumps goauthentik/fips-debian from `2f19fc1` to `189345a`.

---
updated-dependencies:
- dependency-name: goauthentik/fips-debian
  dependency-version: trixie-slim-fips
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-17 12:28:39 +01:00
dependabot[bot]
d64a3aab39 core: bump astral-sh/uv from 0.9.17 to 0.9.18 (#18898)
Bumps [astral-sh/uv](https://github.com/astral-sh/uv) from 0.9.17 to 0.9.18.
- [Release notes](https://github.com/astral-sh/uv/releases)
- [Changelog](https://github.com/astral-sh/uv/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/uv/compare/0.9.17...0.9.18)

---
updated-dependencies:
- dependency-name: astral-sh/uv
  dependency-version: 0.9.18
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-17 12:28:28 +01:00
Dominic R
970cddae47 website/integrations: bookstack: fix redir url (#18891) 2025-12-17 08:42:50 +00:00
authentik-automation[bot]
24c4495ac2 core, web: update translations (#18807)
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
2025-12-17 00:42:03 +00:00
Dewi Roberts
ff38607fa3 web/admin: endpoint: change wording and add helper text (#18871)
* Change wording and add helper text

* Fix helper text

* Setup text change

* Update web/src/admin/endpoints/connectors/agent/AgentConnectorSetup.ts

Co-authored-by: Dominic R <dominic@sdko.org>
Signed-off-by: Dewi Roberts <dewi@goauthentik.io>

* Update web/src/admin/endpoints/connectors/agent/AgentConnectorSetup.ts

Signed-off-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com>

* Update web/src/admin/endpoints/connectors/agent/EnrollmentTokenForm.ts

Signed-off-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com>

* Format.

---------

Signed-off-by: Dewi Roberts <dewi@goauthentik.io>
Signed-off-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com>
Co-authored-by: Dominic R <dominic@sdko.org>
Co-authored-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com>
Co-authored-by: Teffen Ellis <teffen@sister.software>
2025-12-16 23:28:29 +00:00
Dominic R
eef8e57f6c web: fix file upload form (#18808)
* web: fix file upload form name mismatch and modal submit promise handling

Fixes the following error:

FileUploadForm.ts:74  POST http://authentik.localhost:9000/api/v3/admin/file/ 405 (Method Not Allowed)
(anonymous) @ fetch.ts:81
fetchApi @ runtime.ts:206
await in fetchApi
request @ runtime.ts:136
await in request
adminFileCreateRaw @ AdminApi.ts:191
adminFileCreate @ AdminApi.ts:206
send @ FileUploadForm.ts:74
submit @ Form.ts:363
(anonymous) @ ModalForm.ts:54
handleEvent @ lit-html.ts:2109
n @ helpers.ts:117Understand this error
Form.ts:403 authentik/forms: API rejected the form submission due to an invalid field that doesn't appear to be in the form. This is likely a bug in authentik. {detail: 'Response returned an error code'}
(anonymous) @ console.ts:39
(anonymous) @ Form.ts:403
Promise.catch
submit @ Form.ts:376
(anonymous) @ ModalForm.ts:54
handleEvent @ lit-html.ts:2109
n @ helpers.ts:117Understand this error
runtime.ts:140 Uncaught (in promise) ResponseError: Response returned an error code
    at mR.request (runtime.ts:140:15)
    at async mR.adminFileCreateRaw (AdminApi.ts:191:26)
    at async mR.adminFileCreate (AdminApi.ts:206:9)

- align file upload rename field with api name so validation errors map correctly
-improve custom filename extension logic to avoid double or incorrect  extensions
- prevent unhandled promise rejections from modal submit click handler and show  missing-form errors to users

* rev

* wip

* Update ModalForm.ts

Signed-off-by: Dominic R <dominic@sdko.org>

* scope better

* fix what it validates against

---------

Signed-off-by: Dominic R <dominic@sdko.org>
2025-12-16 18:37:22 +00:00
Jens L.
603820854b stages/authenticator_*: fix code input field not string (#18875)
* stages/authenticator_*: fix code input field not string

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* Update authentik/stages/authenticator_totp/stage.py

Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Signed-off-by: Jens L. <jens@beryju.org>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Signed-off-by: Jens L. <jens@beryju.org>
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2025-12-16 19:10:48 +01:00
dependabot[bot]
4ad7f8be2a web: bump vite from 7.2.7 to 7.3.0 in /web (#18854)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 7.2.7 to 7.3.0.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v7.3.0/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v7.3.0/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 7.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-16 18:44:54 +01:00
Alexander Tereshkin
a605cd1e87 web: fix notification counter (#18781) 2025-12-16 18:41:11 +01:00
Alexander Tereshkin
936789f534 web: add custom message with links for empty data export list (#18830) 2025-12-16 17:36:12 +01:00
Jens L.
2f52d832ab website/docs: 2025.10.3 release notes (#18868)
* website/docs: 2025.10.3 release notes

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* format

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-12-16 16:56:57 +01:00
Marcelo Elizeche Landó
036514730e website/docs: Add docs for passkey autofill (WebauthN Conditional UI) (#18805)
* Add docs for passkey autofill feature

* Apply suggestions from code review

Co-authored-by: Dominic R <dominic@sdko.org>
Signed-off-by: Marcelo Elizeche Landó <marce@melizeche.com>

* improve configuration section

* remove blank lines

---------

Signed-off-by: Marcelo Elizeche Landó <marce@melizeche.com>
Co-authored-by: Dominic R <dominic@sdko.org>
2025-12-16 15:16:12 +00:00
Simonyi Gergő
d48129ba7b website/docs: adjust RBAC-related details in 2025.12 release notes (#18863)
* website/docs: adjust RBAC-related details in 2025.12 release notes

* adjust wording

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Simonyi Gergő <28359278+gergosimonyi@users.noreply.github.com>

---------

Signed-off-by: Simonyi Gergő <28359278+gergosimonyi@users.noreply.github.com>
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
2025-12-16 15:02:00 +00:00
Jens L.
d219f72ed6 outposts: fix permission errors for related certificates (#18861)
* outposts: fix permission errors for related certificates

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix tests

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-12-16 15:01:25 +01:00
Simonyi Gergő
7b19045431 web/admin/rbac: misc object permission fixes (#18859)
* rbac: relabel "Assign role permissions" button

* fix object permissions in permissions table

This should have a backend-based fix in the future.
2025-12-16 14:33:25 +01:00
dependabot[bot]
0027813e4b core: bump library/golang from 5d35fb8 to 8e8f9c8 (#18855)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-16 13:09:10 +00:00
Simonyi Gergő
a6ebf1074f rbac: alter migrated direct permission roles (#18860)
These should not be marked as managed, because many of these permissions
were created by admins.
2025-12-16 13:40:49 +01:00
Marcelo Elizeche Landó
ea9689c493 core: add skip s3_test_server_available to TestResolveFileUrlS3Backend (#18858)
add skip s3_test_server_available to TestResolveFileUrlS3Backend
2025-12-16 12:59:13 +01:00
Jens L.
06e7335618 ci: replace codecov test-results action (#18862)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-12-16 12:50:54 +01:00
105 changed files with 1935 additions and 4611 deletions

View File

@@ -12,11 +12,11 @@ runs:
with:
flags: ${{ inputs.flags }}
use_oidc: true
- uses: codecov/test-results-action@47f89e9acb64b76debcd5ea40642d25a4adced9f # v1
- uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5
with:
flags: ${{ inputs.flags }}
file: unittest.xml
use_oidc: true
report_type: test_results
- name: PostgreSQL Logs
shell: bash
run: |

View File

@@ -49,8 +49,12 @@ jobs:
test:
name: Pre-release test
runs-on: ubuntu-latest
needs:
- check-inputs
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5
with:
ref: "version-${{ needs.check-inputs.outputs.major_version }}"
- run: make test-docker
bump-authentik:
name: Bump authentik version

View File

@@ -26,7 +26,7 @@ RUN npm run build && \
npm run build:sfe
# Stage 2: Build go proxy
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.25.5-trixie@sha256:5d35fb8d28b9095d123b7d96095bbf3750ff18be0a87e5a21c9cffc4351fbf96 AS go-builder
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.25.5-trixie@sha256:8e8f9c84609b6005af0a4a8227cee53d6226aab1c6dcb22daf5aeeb8b05480e1 AS go-builder
ARG TARGETOS
ARG TARGETARCH
@@ -76,7 +76,7 @@ RUN --mount=type=secret,id=GEOIPUPDATE_ACCOUNT_ID \
/bin/sh -c "GEOIPUPDATE_LICENSE_KEY_FILE=/run/secrets/GEOIPUPDATE_LICENSE_KEY /usr/bin/entry.sh || echo 'Failed to get GeoIP database, disabling'; exit 0"
# Stage 4: Download uv
FROM ghcr.io/astral-sh/uv:0.9.17@sha256:5cb6b54d2bc3fe2eb9a8483db958a0b9eebf9edff68adedb369df8e7b98711a2 AS uv
FROM ghcr.io/astral-sh/uv:0.9.18@sha256:5713fa8217f92b80223bc83aac7db36ec80a84437dbc0d04bbc659cae030d8c9 AS uv
# Stage 5: Base python image
FROM ghcr.io/goauthentik/fips-python:3.13.9-slim-trixie-fips@sha256:700fc8c1e290bd14e5eaca50b1d8e8c748c820010559cbfb4c4f8dfbe2c4c9ff AS python-base

View File

@@ -37,7 +37,7 @@ class VersionSerializer(PassiveSerializer):
def get_version_latest(self, _) -> str:
"""Get latest version from cache"""
if get_current_tenant().schema_name == get_public_schema_name():
if get_current_tenant().schema_name != get_public_schema_name():
return authentik_version()
version_in_cache = cache.get(VERSION_CACHE_KEY)
if not version_in_cache: # pragma: no cover

View File

@@ -1,10 +1,16 @@
"""Test file service layer"""
from unittest import skipUnless
from django.http import HttpRequest
from django.test import TestCase
from authentik.admin.files.manager import FileManager
from authentik.admin.files.tests.utils import FileTestFileBackendMixin, FileTestS3BackendMixin
from authentik.admin.files.tests.utils import (
FileTestFileBackendMixin,
FileTestS3BackendMixin,
s3_test_server_available,
)
from authentik.admin.files.usage import FileUsage
from authentik.lib.config import CONFIG
@@ -81,6 +87,7 @@ class TestResolveFileUrlFileBackend(FileTestFileBackendMixin, TestCase):
self.assertEqual(result, "http://example.com/files/media/public/test.png")
@skipUnless(s3_test_server_available(), "S3 test server not available")
class TestResolveFileUrlS3Backend(FileTestS3BackendMixin, TestCase):
@CONFIG.patch("storage.media.s3.custom_domain", "s3.test:8080/test")
@CONFIG.patch("storage.media.s3.secure_urls", False)

View File

@@ -15,7 +15,9 @@ class Pagination(pagination.PageNumberPagination):
def get_page_size(self, request):
if self.page_size_query_param in request.query_params:
return min(super().get_page_size(request), request.tenant.pagination_max_page_size)
page_size = super().get_page_size(request)
if page_size is not None:
return min(super().get_page_size(request), request.tenant.pagination_max_page_size)
return request.tenant.pagination_default_page_size
def get_paginated_response(self, data):

View File

@@ -33,6 +33,16 @@ from authentik.endpoints.connectors.agent.auth import AgentAuth
from authentik.rbac.api.roles import RoleSerializer
from authentik.rbac.decorators import permission_required
PARTIAL_USER_SERIALIZER_MODEL_FIELDS = [
"pk",
"username",
"name",
"is_active",
"last_login",
"email",
"attributes",
]
class PartialUserSerializer(ModelSerializer):
"""Partial User Serializer, does not include child relations."""
@@ -42,16 +52,7 @@ class PartialUserSerializer(ModelSerializer):
class Meta:
model = User
fields = [
"pk",
"username",
"name",
"is_active",
"last_login",
"email",
"attributes",
"uid",
]
fields = PARTIAL_USER_SERIALIZER_MODEL_FIELDS + ["uid"]
class RelatedGroupSerializer(ModelSerializer):
@@ -246,7 +247,11 @@ class GroupViewSet(UsedByMixin, ModelViewSet):
]
def get_ql_fields(self):
from akql.schema import BoolField, JSONSearchField, StrField
from djangoql.schema import BoolField, StrField
from authentik.enterprise.search.fields import (
JSONSearchField,
)
return [
StrField(Group, "name"),
@@ -258,7 +263,14 @@ class GroupViewSet(UsedByMixin, ModelViewSet):
base_qs = Group.objects.all().prefetch_related("roles")
if self.serializer_class(context={"request": self.request})._should_include_users:
base_qs = base_qs.prefetch_related("users")
# Only fetch fields needed by PartialUserSerializer to reduce DB load and instantiation
# time
base_qs = base_qs.prefetch_related(
Prefetch(
"users",
queryset=User.objects.all().only(*PARTIAL_USER_SERIALIZER_MODEL_FIELDS),
)
)
else:
base_qs = base_qs.prefetch_related(
Prefetch("users", queryset=User.objects.all().only("id"))

View File

@@ -504,7 +504,12 @@ class UserViewSet(
]
def get_ql_fields(self):
from akql.schema import BoolField, ChoiceSearchField, JSONSearchField, StrField
from djangoql.schema import BoolField, StrField
from authentik.enterprise.search.fields import (
ChoiceSearchField,
JSONSearchField,
)
return [
StrField(User, "username"),

View File

@@ -18,10 +18,9 @@ def migrate_object_permissions(apps: Apps, schema_editor: BaseDatabaseSchemaEdit
RoleModelPermission = apps.get_model("guardian", "RoleModelPermission")
def get_role_for_user_id(user_id: int) -> Role:
name = f"ak-managed-role--user-{user_id}"
name = f"ak-migrated-role--user-{user_id}"
role, created = Role.objects.using(db_alias).get_or_create(
name=name,
managed=name,
)
if created:
role.users.add(user_id)
@@ -32,11 +31,10 @@ def migrate_object_permissions(apps: Apps, schema_editor: BaseDatabaseSchemaEdit
if not role:
# Every django group should already have a role, so this should never happen.
# But let's be nice.
name = f"ak-managed-role--group-{group_id}"
name = f"ak-migrated-role--group-{group_id}"
role, created = Role.objects.using(db_alias).get_or_create(
group_id=group_id,
name=name,
managed=name,
)
if created:
role.group_id = group_id

View File

@@ -0,0 +1,128 @@
"""DjangoQL search"""
from collections import OrderedDict, defaultdict
from collections.abc import Generator
from django.db import connection
from django.db.models import Model, Q
from djangoql.compat import text_type
from djangoql.schema import StrField
class JSONSearchField(StrField):
"""JSON field for DjangoQL"""
model: Model
def __init__(self, model=None, name=None, nullable=None, suggest_nested=True):
# Set this in the constructor to not clobber the type variable
self.type = "relation"
self.suggest_nested = suggest_nested
super().__init__(model, name, nullable)
def get_lookup(self, path, operator, value):
search = "__".join(path)
op, invert = self.get_operator(operator)
q = Q(**{f"{search}{op}": self.get_lookup_value(value)})
return ~q if invert else q
def json_field_keys(self) -> Generator[tuple[str]]:
with connection.cursor() as cursor:
cursor.execute(
f"""
WITH RECURSIVE "{self.name}_keys" AS (
SELECT
ARRAY[jsonb_object_keys("{self.name}")] AS key_path_array,
"{self.name}" -> jsonb_object_keys("{self.name}") AS value
FROM {self.model._meta.db_table}
WHERE "{self.name}" IS NOT NULL
AND jsonb_typeof("{self.name}") = 'object'
UNION ALL
SELECT
ck.key_path_array || jsonb_object_keys(ck.value),
ck.value -> jsonb_object_keys(ck.value) AS value
FROM "{self.name}_keys" ck
WHERE jsonb_typeof(ck.value) = 'object'
),
unique_paths AS (
SELECT DISTINCT key_path_array
FROM "{self.name}_keys"
)
SELECT key_path_array FROM unique_paths;
""" # nosec
)
return (x[0] for x in cursor.fetchall())
def get_nested_options(self) -> OrderedDict:
"""Get keys of all nested objects to show autocomplete"""
if not self.suggest_nested:
return OrderedDict()
base_model_name = f"{self.model._meta.app_label}.{self.model._meta.model_name}_{self.name}"
def recursive_function(parts: list[str], parent_parts: list[str] | None = None):
if not parent_parts:
parent_parts = []
path = parts.pop(0)
parent_parts.append(path)
relation_key = "_".join(parent_parts)
if len(parts) > 1:
out_dict = {
relation_key: {
parts[0]: {
"type": "relation",
"relation": f"{relation_key}_{parts[0]}",
}
}
}
child_paths = recursive_function(parts.copy(), parent_parts.copy())
child_paths.update(out_dict)
return child_paths
else:
return {relation_key: {parts[0]: {}}}
relation_structure = defaultdict(dict)
for relations in self.json_field_keys():
result = recursive_function([base_model_name] + relations)
for relation_key, value in result.items():
for sub_relation_key, sub_value in value.items():
if not relation_structure[relation_key].get(sub_relation_key, None):
relation_structure[relation_key][sub_relation_key] = sub_value
else:
relation_structure[relation_key][sub_relation_key].update(sub_value)
final_dict = defaultdict(dict)
for key, value in relation_structure.items():
for sub_key, sub_value in value.items():
if not sub_value:
final_dict[key][sub_key] = {
"type": "str",
"nullable": True,
}
else:
final_dict[key][sub_key] = sub_value
return OrderedDict(final_dict)
def relation(self) -> str:
return f"{self.model._meta.app_label}.{self.model._meta.model_name}_{self.name}"
class ChoiceSearchField(StrField):
def __init__(self, model=None, name=None, nullable=None):
super().__init__(model, name, nullable, suggest_options=True)
def get_options(self, search):
result = []
choices = self._field_choices()
if choices:
search = search.lower()
for c in choices:
choice = text_type(c[0])
if search in choice.lower():
result.append(choice)
return result

View File

@@ -1,15 +1,18 @@
"""QL search"""
"""DjangoQL search"""
from akql.exceptions import AKQLError
from akql.queryset import apply_search
from akql.schema import AKQLSchema
from django.apps import apps
from django.db.models import QuerySet
from djangoql.ast import Name
from djangoql.exceptions import DjangoQLError
from djangoql.queryset import apply_search
from djangoql.schema import DjangoQLSchema
from drf_spectacular.plumbing import ResolvedComponent, build_object_type
from rest_framework.filters import SearchFilter
from rest_framework.request import Request
from structlog.stdlib import get_logger
from authentik.enterprise.search.fields import JSONSearchField
LOGGER = get_logger()
AUTOCOMPLETE_SCHEMA = ResolvedComponent(
name="Autocomplete",
@@ -19,8 +22,27 @@ AUTOCOMPLETE_SCHEMA = ResolvedComponent(
)
class BaseSchema(DjangoQLSchema):
"""Base Schema which deals with JSON Fields"""
def resolve_name(self, name: Name):
model = self.model_label(self.current_model)
root_field = name.parts[0]
field = self.models[model].get(root_field)
# If the query goes into a JSON field, return the root
# field as the JSON field will do the rest
if isinstance(field, JSONSearchField):
# This is a workaround; build_filter will remove the right-most
# entry in the path as that is intended to be the same as the field
# however for JSON that is not the case
if name.parts[-1] != root_field:
name.parts.append(root_field)
return field
return super().resolve_name(name)
class QLSearch(SearchFilter):
"""rest_framework search filter which uses AKQL"""
"""rest_framework search filter which uses DjangoQL"""
def __init__(self):
super().__init__()
@@ -37,30 +59,24 @@ class QLSearch(SearchFilter):
params = params.replace("\x00", "") # strip null characters
return params
def get_schema(self, request: Request, view) -> AKQLSchema:
def get_schema(self, request: Request, view) -> BaseSchema:
ql_fields = []
if hasattr(view, "get_ql_fields"):
ql_fields = view.get_ql_fields()
class InlineSchema(AKQLSchema):
class InlineSchema(BaseSchema):
def get_fields(self, model):
return ql_fields or []
return InlineSchema
def get_search_context(self, request: Request):
return {
"$ak_user": request.user.pk,
}
def filter_queryset(self, request: Request, queryset: QuerySet, view) -> QuerySet:
search_query = self.get_search_terms(request)
schema = self.get_schema(request, view)
if len(search_query) == 0 or not self.enabled:
return self._fallback.filter_queryset(request, queryset, view)
context = self.get_search_context(request)
try:
return apply_search(queryset, search_query, context=context, schema=schema)
except AKQLError as exc:
return apply_search(queryset, search_query, schema=schema)
except DjangoQLError as exc:
LOGGER.debug("Failed to parse search expression", exc=exc)
return self._fallback.filter_queryset(request, queryset, view)

View File

@@ -1,11 +1,11 @@
from akql.schema import JSONSearchField
from akql.serializers import AKQLSchemaSerializer
from djangoql.serializers import DjangoQLSchemaSerializer
from drf_spectacular.generators import SchemaGenerator
from authentik.enterprise.search.fields import JSONSearchField
from authentik.enterprise.search.ql import AUTOCOMPLETE_SCHEMA
class AKQLSchemaSerializer(AKQLSchemaSerializer):
class AKQLSchemaSerializer(DjangoQLSchemaSerializer):
def serialize(self, schema):
serialization = super().serialize(schema)
for _, fields in schema.models.items():
@@ -15,6 +15,12 @@ class AKQLSchemaSerializer(AKQLSchemaSerializer):
serialization["models"].update(field.get_nested_options())
return serialization
def serialize_field(self, field):
result = super().serialize_field(field)
if isinstance(field, JSONSearchField):
result["relation"] = field.relation()
return result
def postprocess_schema_search_autocomplete(result, generator: SchemaGenerator, **kwargs):
generator.registry.register_on_missing(AUTOCOMPLETE_SCHEMA)

View File

@@ -136,7 +136,9 @@ class EventViewSet(
filterset_class = EventsFilter
def get_ql_fields(self):
from akql.schema import ChoiceSearchField, DateTimeField, JSONSearchField, StrField
from djangoql.schema import DateTimeField, StrField
from authentik.enterprise.search.fields import ChoiceSearchField, JSONSearchField
return [
ChoiceSearchField(Event, "action"),

18
authentik/flows/auth.py Normal file
View File

@@ -0,0 +1,18 @@
from typing import cast
from django.contrib.auth.models import AnonymousUser
from rest_framework.authentication import BaseAuthentication
from rest_framework.request import Request
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
from authentik.flows.views.executor import SESSION_KEY_PLAN
class FlowActive(BaseAuthentication):
"""Authenticate requests when a flow is currently active"""
def authenticate(self, request: Request):
plan = cast(FlowPlan | None, request.session.get(SESSION_KEY_PLAN))
if not plan:
return None
return (plan.context.get(PLAN_CONTEXT_PENDING_USER, AnonymousUser()), plan)

View File

@@ -249,7 +249,7 @@ class ChallengeStageView(StageView):
"f(ch): invalid challenge response",
errors=challenge_response.errors,
)
return HttpChallengeResponse(challenge_response)
return HttpChallengeResponse(challenge_response, status=400)
class AccessDeniedStage(ChallengeStageView):

View File

@@ -48,6 +48,9 @@ class FlowTestCase(APITestCase):
self.assertEqual(raw_response[key], expected)
return raw_response
def get_flow_plan(self) -> FlowPlan | None:
return self.client.session.get(SESSION_KEY_PLAN)
def assertStageRedirects(self, response: HttpResponse, to: str) -> dict[str, Any]:
"""Wrapper around assertStageResponse that checks for a redirect"""
return self.assertStageResponse(response, component="xak-flow-redirect", to=to)

View File

@@ -147,6 +147,8 @@ class FlowExecutorView(APIView):
token.delete()
if not isinstance(plan, FlowPlan):
return None
if existing_plan := self.request.session[SESSION_KEY_PLAN]:
plan.context.update(existing_plan.context)
plan.context[PLAN_CONTEXT_IS_RESTORED] = token
self._logger.debug("f(exec): restored flow plan from token", plan=plan)
return plan
@@ -256,6 +258,11 @@ class FlowExecutorView(APIView):
serializers=challenge_types,
resource_type_field_name="component",
),
400: PolymorphicProxySerializer(
component_name="ChallengeTypes",
serializers=challenge_types,
resource_type_field_name="component",
),
},
request=OpenApiTypes.NONE,
parameters=[
@@ -303,6 +310,11 @@ class FlowExecutorView(APIView):
serializers=challenge_types,
resource_type_field_name="component",
),
400: PolymorphicProxySerializer(
component_name="ChallengeTypes",
serializers=challenge_types,
resource_type_field_name="component",
),
},
request=PolymorphicProxySerializer(
component_name="FlowChallengeResponse",

View File

@@ -86,7 +86,7 @@ class OutpostConfig:
class OutpostModel(Model):
"""Base model for providers that need more objects than just themselves"""
def get_required_objects(self) -> Iterable[models.Model | str]:
def get_required_objects(self) -> Iterable[models.Model | str | tuple[str, models.Model]]:
"""Return a list of all required objects"""
return [self]
@@ -332,41 +332,35 @@ class Outpost(ScheduledModel, SerializerModel, ManagedModel):
"""Create per-object and global permissions for outpost service-account"""
# To ensure the user only has the correct permissions, we delete all of them and re-add
# the ones the user needs
with transaction.atomic():
user.remove_all_perms_from_managed_role()
for model_or_perm in self.get_required_objects():
if isinstance(model_or_perm, models.Model):
model_or_perm: models.Model
code_name = (
f"{model_or_perm._meta.app_label}.view_{model_or_perm._meta.model_name}"
)
try:
user.assign_perms_to_managed_role(code_name, model_or_perm)
except (Permission.DoesNotExist, AttributeError) as exc:
LOGGER.warning(
"permission doesn't exist",
code_name=code_name,
user=user,
model=model_or_perm,
try:
with transaction.atomic():
user.remove_all_perms_from_managed_role()
for model_or_perm in self.get_required_objects():
if isinstance(model_or_perm, models.Model):
code_name = (
f"{model_or_perm._meta.app_label}.view_{model_or_perm._meta.model_name}"
)
Event.new(
action=EventAction.SYSTEM_EXCEPTION,
message=(
"While setting the permissions for the service-account, a "
"permission was not found: Check "
"https://docs.goauthentik.io/troubleshooting/missing_permission"
),
).with_exception(exc).set_user(user).save()
else:
app_label, perm = model_or_perm.split(".")
permission = Permission.objects.filter(
codename=perm,
content_type__app_label=app_label,
)
if not permission.exists():
LOGGER.warning("permission doesn't exist", perm=model_or_perm)
continue
user.assign_perms_to_managed_role(permission.first())
user.assign_perms_to_managed_role(code_name, model_or_perm)
elif isinstance(model_or_perm, tuple):
perm, obj = model_or_perm
user.assign_perms_to_managed_role(perm, obj)
else:
user.assign_perms_to_managed_role(model_or_perm)
except (Permission.DoesNotExist, AttributeError) as exc:
LOGGER.warning(
"permission doesn't exist",
code_name=code_name,
user=user,
model=model_or_perm,
)
Event.new(
action=EventAction.SYSTEM_EXCEPTION,
message=(
"While setting the permissions for the service-account, a "
"permission was not found: Check "
"https://docs.goauthentik.io/troubleshooting/missing_permission"
),
).with_exception(exc).set_user(user).save()
LOGGER.debug(
"Updated service account's permissions",
obj_perms=user.get_all_obj_perms_on_managed_role(),
@@ -431,7 +425,7 @@ class Outpost(ScheduledModel, SerializerModel, ManagedModel):
Token.objects.filter(identifier=self.token_identifier).delete()
return self.token
def get_required_objects(self) -> Iterable[models.Model | str]:
def get_required_objects(self) -> Iterable[models.Model | str | tuple[str, models.Model]]:
"""Get an iterator of all objects the user needs read access to"""
objects: list[models.Model | str] = [
self,
@@ -445,7 +439,9 @@ class Outpost(ScheduledModel, SerializerModel, ManagedModel):
if self.managed:
for brand in Brand.objects.filter(web_certificate__isnull=False):
objects.append(brand)
objects.append(brand.web_certificate)
objects.append(("view_certificatekeypair", brand.web_certificate))
objects.append(("view_certificatekeypair_certificate", brand.web_certificate))
objects.append(("view_certificatekeypair_key", brand.web_certificate))
return objects
def __str__(self) -> str:

View File

@@ -51,10 +51,12 @@ class OutpostTests(TestCase):
permissions = outpost.user.get_all_obj_perms_on_managed_role().order_by(
"content_type__model"
)
self.assertEqual(len(permissions), 3)
self.assertEqual(len(permissions), 5)
self.assertEqual(permissions[0].object_pk, str(keypair.pk))
self.assertEqual(permissions[1].object_pk, str(outpost.pk))
self.assertEqual(permissions[2].object_pk, str(provider.pk))
self.assertEqual(permissions[1].object_pk, str(keypair.pk))
self.assertEqual(permissions[2].object_pk, str(keypair.pk))
self.assertEqual(permissions[3].object_pk, str(outpost.pk))
self.assertEqual(permissions[4].object_pk, str(provider.pk))
# Remove provider from outpost, user should only have access to outpost
outpost.providers.remove(provider)

View File

@@ -93,11 +93,13 @@ class LDAPProvider(OutpostModel, BackchannelProvider):
def __str__(self):
return f"LDAP Provider {self.name}"
def get_required_objects(self) -> Iterable[models.Model | str]:
required_models = [self, "authentik_core.view_user", "authentik_core.view_group"]
def get_required_objects(self) -> Iterable[models.Model | str | tuple[str, models.Model]]:
required = [self, "authentik_core.view_user", "authentik_core.view_group"]
if self.certificate is not None:
required_models.append(self.certificate)
return required_models
required.append(("view_certificatekeypair", self.certificate))
required.append(("view_certificatekeypair_certificate", self.certificate))
required.append(("view_certificatekeypair_key", self.certificate))
return required
class Meta:
verbose_name = _("LDAP Provider")

View File

@@ -179,11 +179,13 @@ class ProxyProvider(OutpostModel, OAuth2Provider):
def __str__(self):
return f"Proxy Provider {self.name}"
def get_required_objects(self) -> Iterable[models.Model | str]:
required_models = [self]
def get_required_objects(self) -> Iterable[models.Model | str | tuple[str, models.Model]]:
required = [self]
if self.certificate is not None:
required_models.append(self.certificate)
return required_models
required.append(("view_certificatekeypair", self.certificate))
required.append(("view_certificatekeypair_certificate", self.certificate))
required.append(("view_certificatekeypair_key", self.certificate))
return required
class Meta:
verbose_name = _("Proxy Provider")

View File

@@ -1,10 +1,14 @@
"""proxy provider tests"""
from json import loads
from django.urls import reverse
from rest_framework.test import APITestCase
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
from authentik.core.models import Application
from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow
from authentik.lib.generators import generate_id
from authentik.outposts.models import Outpost, OutpostType
from authentik.providers.oauth2.models import ClientTypes
from authentik.providers.proxy.models import ProxyMode, ProxyProvider
@@ -127,3 +131,55 @@ class ProxyProviderTests(APITestCase):
self.assertEqual(response.status_code, 200)
provider: ProxyProvider = ProxyProvider.objects.get(name=name)
self.assertEqual(provider.client_type, ClientTypes.CONFIDENTIAL)
def test_sa_fetch(self):
"""Test fetching the outpost config as the service account"""
outpost = Outpost.objects.create(name=generate_id(), type=OutpostType.PROXY)
provider = ProxyProvider.objects.create(name=generate_id())
Application.objects.create(name=generate_id(), slug=generate_id(), provider=provider)
outpost.providers.add(provider)
res = self.client.get(
reverse("authentik_api:proxyprovideroutpost-list"),
HTTP_AUTHORIZATION=f"Bearer {outpost.token.key}",
)
body = loads(res.content)
self.assertEqual(body["pagination"]["count"], 1)
def test_sa_perms_cert(self):
"""Test permissions to access a configured certificate"""
cert = create_test_cert()
outpost = Outpost.objects.create(name=generate_id(), type=OutpostType.PROXY)
provider = ProxyProvider.objects.create(name=generate_id(), certificate=cert)
Application.objects.create(name=generate_id(), slug=generate_id(), provider=provider)
outpost.providers.add(provider)
res = self.client.get(
reverse("authentik_api:proxyprovideroutpost-list"),
HTTP_AUTHORIZATION=f"Bearer {outpost.token.key}",
)
body = loads(res.content)
self.assertEqual(body["pagination"]["count"], 1)
cert_id = body["results"][0]["certificate"]
self.assertEqual(cert_id, str(cert.pk))
res = self.client.get(
reverse(
"authentik_api:certificatekeypair-view-certificate",
kwargs={
"pk": cert_id,
},
),
HTTP_AUTHORIZATION=f"Bearer {outpost.token.key}",
)
self.assertEqual(res.status_code, 200)
# res = self.client.get(
# reverse(
# "authentik_api:certificatekeypair-view-private-key",
# kwargs={
# "pk": cert_id,
# },
# ),
# HTTP_AUTHORIZATION=f"Bearer {outpost.token.key}",
# )
# self.assertEqual(res.status_code, 200)

View File

@@ -64,10 +64,12 @@ class RadiusProvider(OutpostModel, Provider):
return RadiusProviderSerializer
def get_required_objects(self) -> Iterable[models.Model | str]:
def get_required_objects(self) -> Iterable[models.Model | str | tuple[str, models.Model]]:
required = [self, "authentik_stages_mtls.pass_outpost_certificate"]
if self.certificate is not None:
required.append(self.certificate)
required.append(("view_certificatekeypair", self.certificate))
required.append(("view_certificatekeypair_certificate", self.certificate))
required.append(("view_certificatekeypair_key", self.certificate))
return required
def __str__(self):

View File

@@ -9,7 +9,7 @@ from guardian.shortcuts import get_objects_for_user
from rest_framework import mixins
from rest_framework.decorators import action
from rest_framework.fields import CharField, ChoiceField, IntegerField
from rest_framework.permissions import IsAuthenticated
from rest_framework.permissions import AllowAny
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.viewsets import GenericViewSet, ModelViewSet
@@ -21,9 +21,11 @@ from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer
from authentik.core.models import User
from authentik.flows.api.stages import StageSerializer
from authentik.flows.auth import FlowActive
from authentik.flows.planner import FlowPlan
from authentik.rbac.decorators import permission_required
from authentik.stages.authenticator_duo.models import AuthenticatorDuoStage, DuoDevice
from authentik.stages.authenticator_duo.stage import SESSION_KEY_DUO_ENROLL
from authentik.stages.authenticator_duo.stage import PLAN_CONTEXT_DUO_ENROLL
LOGGER = get_logger()
@@ -84,14 +86,20 @@ class AuthenticatorDuoStageViewSet(UsedByMixin, ModelViewSet):
),
},
)
@action(methods=["POST"], detail=True, permission_classes=[IsAuthenticated])
@action(
methods=["POST"],
detail=True,
authentication_classes=[FlowActive],
permission_classes=[AllowAny],
)
def enrollment_status(self, request: Request, pk: str) -> Response:
"""Check enrollment status of user details in current session"""
stage: AuthenticatorDuoStage = AuthenticatorDuoStage.objects.filter(pk=pk).first()
if not stage:
raise Http404
client = stage.auth_client()
enroll = self.request.session.get(SESSION_KEY_DUO_ENROLL)
plan: FlowPlan = request.auth
enroll = plan.context.get(PLAN_CONTEXT_DUO_ENROLL)
if not enroll:
return Response(status=400)
status = client.enroll_status(enroll["user_id"], enroll["activation_code"])

View File

@@ -14,7 +14,7 @@ from authentik.flows.stage import ChallengeStageView
from authentik.flows.views.executor import InvalidStageError
from authentik.stages.authenticator_duo.models import AuthenticatorDuoStage, DuoDevice
SESSION_KEY_DUO_ENROLL = "authentik/stages/authenticator_duo/enroll"
PLAN_CONTEXT_DUO_ENROLL = "goauthentik.io/stages/authenticator_duo/enroll"
class AuthenticatorDuoChallenge(WithUserInfoChallenge):
@@ -50,14 +50,14 @@ class AuthenticatorDuoStageView(ChallengeStageView):
user=user,
).from_http(self.request, user)
raise InvalidStageError(str(exc)) from exc
self.request.session[SESSION_KEY_DUO_ENROLL] = enroll
self.executor.plan.context[PLAN_CONTEXT_DUO_ENROLL] = enroll
return enroll
def get_challenge(self, *args, **kwargs) -> Challenge:
stage: AuthenticatorDuoStage = self.executor.current_stage
if SESSION_KEY_DUO_ENROLL not in self.request.session:
if PLAN_CONTEXT_DUO_ENROLL not in self.executor.plan.context:
self.duo_enroll()
enroll = self.request.session[SESSION_KEY_DUO_ENROLL]
enroll = self.executor.plan.context[PLAN_CONTEXT_DUO_ENROLL]
return AuthenticatorDuoChallenge(
data={
"activation_barcode": enroll["activation_barcode"],
@@ -69,14 +69,14 @@ class AuthenticatorDuoStageView(ChallengeStageView):
def challenge_valid(self, response: ChallengeResponse) -> HttpResponse:
# Duo Challenge has already been validated
stage: AuthenticatorDuoStage = self.executor.current_stage
enroll = self.request.session.get(SESSION_KEY_DUO_ENROLL)
enroll = self.executor.plan.context.get(PLAN_CONTEXT_DUO_ENROLL)
enroll_status = stage.auth_client().enroll_status(
enroll["user_id"], enroll["activation_code"]
)
if enroll_status != "success":
return self.executor.stage_invalid(f"Invalid enrollment status: {enroll_status}.")
existing_device = DuoDevice.objects.filter(duo_user_id=enroll["user_id"]).first()
self.request.session.pop(SESSION_KEY_DUO_ENROLL)
self.executor.plan.context.pop(PLAN_CONTEXT_DUO_ENROLL)
if not existing_device:
DuoDevice.objects.create(
name="Duo Authenticator",
@@ -88,6 +88,3 @@ class AuthenticatorDuoStageView(ChallengeStageView):
else:
return self.executor.stage_invalid("Device with Credential ID already exists.")
return self.executor.stage_ok()
def cleanup(self):
self.request.session.pop(SESSION_KEY_DUO_ENROLL, None)

View File

@@ -11,7 +11,6 @@ from authentik.flows.models import FlowStageBinding
from authentik.flows.tests import FlowTestCase
from authentik.lib.generators import generate_id
from authentik.stages.authenticator_duo.models import AuthenticatorDuoStage, DuoDevice
from authentik.stages.authenticator_duo.stage import SESSION_KEY_DUO_ENROLL
from authentik.stages.identification.models import IdentificationStage, UserFields
@@ -51,42 +50,6 @@ class AuthenticatorDuoStageTests(FlowTestCase):
)
self.assertEqual(response.status_code, 404)
def test_api_enrollment(self):
"""Test `enrollment_status`"""
self.client.force_login(self.user)
stage = AuthenticatorDuoStage.objects.create(
name=generate_id(),
client_id=generate_id(),
client_secret=generate_id(),
api_hostname=generate_id(),
)
response = self.client.post(
reverse(
"authentik_api:authenticatorduostage-enrollment-status",
kwargs={
"pk": str(stage.pk),
},
)
)
self.assertEqual(response.status_code, 400)
session = self.client.session
session[SESSION_KEY_DUO_ENROLL] = {"user_id": "foo", "activation_code": "bar"}
session.save()
with patch("duo_client.auth.Auth.enroll_status", MagicMock(return_value="foo")):
response = self.client.post(
reverse(
"authentik_api:authenticatorduostage-enrollment-status",
kwargs={
"pk": str(stage.pk),
},
)
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.content.decode(), '{"duo_response":"foo"}')
def test_api_import_manual_invalid_username(self):
"""Test `import_device_manual`"""
self.client.force_login(self.user)
@@ -314,6 +277,17 @@ class AuthenticatorDuoStageTests(FlowTestCase):
self.assertEqual(enroll_mock.call_count, 1)
with patch("duo_client.auth.Auth.enroll_status", MagicMock(return_value="success")):
response = self.client.post(
reverse(
"authentik_api:authenticatorduostage-enrollment-status",
kwargs={
"pk": str(stage.pk),
},
)
)
self.assertEqual(response.status_code, 200)
self.assertJSONEqual(response.content, {"duo_response": "success"})
response = self.client.post(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), {}
)

View File

@@ -6,7 +6,7 @@ from django.http.request import QueryDict
from django.template.exceptions import TemplateSyntaxError
from django.utils.translation import gettext_lazy as _
from rest_framework.exceptions import ValidationError
from rest_framework.fields import BooleanField, CharField, IntegerField
from rest_framework.fields import BooleanField, CharField
from authentik.events.models import Event, EventAction
from authentik.flows.challenge import (
@@ -26,7 +26,7 @@ from authentik.stages.email.tasks import send_mails
from authentik.stages.email.utils import TemplateEmailMessage
from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
SESSION_KEY_EMAIL_DEVICE = "authentik/stages/authenticator_email/email_device"
PLAN_CONTEXT_EMAIL_DEVICE = "goauthentik.io/stages/authenticator_email/email_device"
PLAN_CONTEXT_EMAIL = "email"
PLAN_CONTEXT_EMAIL_SENT = "email_sent"
PLAN_CONTEXT_EMAIL_OVERRIDE = "email"
@@ -47,7 +47,7 @@ class AuthenticatorEmailChallengeResponse(ChallengeResponse):
device: EmailDevice
code = IntegerField(required=False)
code = CharField(required=False)
email = CharField(required=False)
component = CharField(default="ak-stage-authenticator-email")
@@ -79,7 +79,7 @@ class AuthenticatorEmailStageView(ChallengeStageView):
if EmailDevice.objects.filter(Q(email=email), stage=stage.pk).exists():
raise ValidationError(_("Invalid email"))
device: EmailDevice = self.request.session[SESSION_KEY_EMAIL_DEVICE]
device: EmailDevice = self.executor.plan.context[PLAN_CONTEXT_EMAIL_DEVICE]
try:
message = TemplateEmailMessage(
@@ -116,9 +116,9 @@ class AuthenticatorEmailStageView(ChallengeStageView):
self.logger.debug("got email from plan context")
return context.get(PLAN_CONTEXT_PROMPT, {}).get(PLAN_CONTEXT_EMAIL)
# Check device for email
if SESSION_KEY_EMAIL_DEVICE in self.request.session:
if PLAN_CONTEXT_EMAIL_DEVICE in self.executor.plan.context:
self.logger.debug("got email from device in session")
device: EmailDevice = self.request.session[SESSION_KEY_EMAIL_DEVICE]
device: EmailDevice = self.executor.plan.context[PLAN_CONTEXT_EMAIL_DEVICE]
if device.email == "":
return None
return device.email
@@ -135,7 +135,7 @@ class AuthenticatorEmailStageView(ChallengeStageView):
def get_response_instance(self, data: QueryDict) -> ChallengeResponse:
response = super().get_response_instance(data)
response.device = self.request.session[SESSION_KEY_EMAIL_DEVICE]
response.device = self.executor.plan.context[PLAN_CONTEXT_EMAIL_DEVICE]
return response
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
@@ -147,11 +147,11 @@ class AuthenticatorEmailStageView(ChallengeStageView):
return self.executor.stage_invalid(
_("The user already has an email address registered for MFA.")
)
if SESSION_KEY_EMAIL_DEVICE not in self.request.session:
if PLAN_CONTEXT_EMAIL_DEVICE not in self.executor.plan.context:
device = EmailDevice(user=user, confirmed=False, stage=stage, name="Email Device")
valid_secs: int = timedelta_from_string(stage.token_expiry).total_seconds()
device.generate_token(valid_secs=valid_secs, commit=False)
self.request.session[SESSION_KEY_EMAIL_DEVICE] = device
self.executor.plan.context[PLAN_CONTEXT_EMAIL_DEVICE] = device
if email := self._has_email():
device.email = email
try:
@@ -165,16 +165,16 @@ class AuthenticatorEmailStageView(ChallengeStageView):
self.executor.plan.context.get(PLAN_CONTEXT_PROMPT, {}).pop(
PLAN_CONTEXT_EMAIL, None
)
self.request.session.pop(SESSION_KEY_EMAIL_DEVICE, None)
self.executor.plan.context.pop(PLAN_CONTEXT_EMAIL_DEVICE, None)
self.logger.warning("failed to send email to pre-set address", exc=exc)
return self.get(request, *args, **kwargs)
return super().get(request, *args, **kwargs)
def challenge_valid(self, response: ChallengeResponse) -> HttpResponse:
"""Email Token is validated by challenge"""
device: EmailDevice = self.request.session[SESSION_KEY_EMAIL_DEVICE]
device: EmailDevice = self.executor.plan.context[PLAN_CONTEXT_EMAIL_DEVICE]
if not device.confirmed:
return self.challenge_invalid(response)
device.save()
del self.request.session[SESSION_KEY_EMAIL_DEVICE]
del self.executor.plan.context[PLAN_CONTEXT_EMAIL_DEVICE]
return self.executor.stage_ok()

View File

@@ -11,7 +11,7 @@ from django.template.exceptions import TemplateDoesNotExist
from django.urls import reverse
from django.utils.timezone import now
from authentik.core.tests.utils import create_test_admin_user, create_test_flow, create_test_user
from authentik.core.tests.utils import create_test_flow, create_test_user
from authentik.flows.models import FlowStageBinding
from authentik.flows.tests import FlowTestCase
from authentik.lib.config import CONFIG
@@ -21,9 +21,7 @@ from authentik.stages.authenticator_email.api import (
EmailDeviceSerializer,
)
from authentik.stages.authenticator_email.models import AuthenticatorEmailStage, EmailDevice
from authentik.stages.authenticator_email.stage import (
SESSION_KEY_EMAIL_DEVICE,
)
from authentik.stages.authenticator_email.stage import PLAN_CONTEXT_EMAIL_DEVICE
from authentik.stages.email.utils import TemplateEmailMessage
@@ -33,7 +31,7 @@ class TestAuthenticatorEmailStage(FlowTestCase):
def setUp(self):
super().setUp()
self.flow = create_test_flow()
self.user = create_test_admin_user()
self.user = create_test_user()
self.user_noemail = create_test_user(email="")
self.stage = AuthenticatorEmailStage.objects.create(
name="email-authenticator",
@@ -213,20 +211,26 @@ class TestAuthenticatorEmailStage(FlowTestCase):
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
data={"component": "ak-stage-authenticator-email"},
)
self.assertIn("email required", str(response.content))
self.assertStageResponse(
response,
self.flow,
response_errors={"non_field_errors": [{"code": "invalid", "string": "email required"}]},
)
# Test invalid code
response = self.client.post(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
data={"component": "ak-stage-authenticator-email", "code": "000000"},
)
self.assertIn("Code does not match", str(response.content))
self.assertStageResponse(
response,
self.flow,
response_errors={
"non_field_errors": [{"code": "invalid", "string": "Code does not match"}]
},
)
# Test valid code
self.client.force_login(self.user)
response = self.client.get(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
)
device = self.device
token = device.token
response = self.client.post(
@@ -285,8 +289,7 @@ class TestAuthenticatorEmailStage(FlowTestCase):
response = self.client.get(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
)
self.assertIn(SESSION_KEY_EMAIL_DEVICE, self.client.session)
device = self.client.session[SESSION_KEY_EMAIL_DEVICE]
device = self.get_flow_plan().context[PLAN_CONTEXT_EMAIL_DEVICE]
self.assertIsInstance(device, EmailDevice)
self.assertFalse(device.confirmed)
self.assertEqual(device.user, self.user)
@@ -294,8 +297,6 @@ class TestAuthenticatorEmailStage(FlowTestCase):
# Test device confirmation and cleanup
device.confirmed = True
device.email = "new_test@authentik.local" # Use a different email
self.client.session[SESSION_KEY_EMAIL_DEVICE] = device
self.client.session.save()
response = self.client.post(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
data={"component": "ak-stage-authenticator-email", "code": device.token},

View File

@@ -5,7 +5,7 @@ from django.http import HttpRequest, HttpResponse
from django.http.request import QueryDict
from django.utils.translation import gettext_lazy as _
from rest_framework.exceptions import ValidationError
from rest_framework.fields import BooleanField, CharField, IntegerField
from rest_framework.fields import BooleanField, CharField
from authentik.flows.challenge import (
Challenge,
@@ -20,7 +20,7 @@ from authentik.stages.authenticator_sms.models import (
)
from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
SESSION_KEY_SMS_DEVICE = "authentik/stages/authenticator_sms/sms_device"
PLAN_CONTEXT_SMS_DEVICE = "goauthentik.io/stages/authenticator_sms/sms_device"
PLAN_CONTEXT_PHONE = "phone"
@@ -38,7 +38,7 @@ class AuthenticatorSMSChallengeResponse(ChallengeResponse):
device: SMSDevice
code = IntegerField(required=False)
code = CharField(required=False)
phone_number = CharField(required=False)
component = CharField(default="ak-stage-authenticator-sms")
@@ -70,7 +70,7 @@ class AuthenticatorSMSStageView(ChallengeStageView):
if SMSDevice.objects.filter(query, stage=stage.pk).exists():
raise ValidationError(_("Invalid phone number"))
# No code yet, but we have a phone number, so send a verification message
device: SMSDevice = self.request.session[SESSION_KEY_SMS_DEVICE]
device: SMSDevice = self.executor.plan.context[PLAN_CONTEXT_SMS_DEVICE]
stage.send(self.request, device.token, device)
def _has_phone_number(self) -> str | None:
@@ -78,9 +78,9 @@ class AuthenticatorSMSStageView(ChallengeStageView):
if PLAN_CONTEXT_PHONE in context.get(PLAN_CONTEXT_PROMPT, {}):
self.logger.debug("got phone number from plan context")
return context.get(PLAN_CONTEXT_PROMPT, {}).get(PLAN_CONTEXT_PHONE)
if SESSION_KEY_SMS_DEVICE in self.request.session:
if PLAN_CONTEXT_SMS_DEVICE in self.executor.plan.context:
self.logger.debug("got phone number from device in session")
device: SMSDevice = self.request.session[SESSION_KEY_SMS_DEVICE]
device: SMSDevice = self.executor.plan.context[PLAN_CONTEXT_SMS_DEVICE]
if device.phone_number == "":
return None
return device.phone_number
@@ -95,7 +95,7 @@ class AuthenticatorSMSStageView(ChallengeStageView):
def get_response_instance(self, data: QueryDict) -> ChallengeResponse:
response = super().get_response_instance(data)
response.device = self.request.session[SESSION_KEY_SMS_DEVICE]
response.device = self.executor.plan.context[PLAN_CONTEXT_SMS_DEVICE]
return response
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
@@ -103,10 +103,10 @@ class AuthenticatorSMSStageView(ChallengeStageView):
stage: AuthenticatorSMSStage = self.executor.current_stage
if SESSION_KEY_SMS_DEVICE not in self.request.session:
if PLAN_CONTEXT_SMS_DEVICE not in self.executor.plan.context:
device = SMSDevice(user=user, confirmed=False, stage=stage, name="SMS Device")
device.generate_token(commit=False)
self.request.session[SESSION_KEY_SMS_DEVICE] = device
self.executor.plan.context[PLAN_CONTEXT_SMS_DEVICE] = device
if phone_number := self._has_phone_number():
device.phone_number = phone_number
try:
@@ -120,14 +120,14 @@ class AuthenticatorSMSStageView(ChallengeStageView):
self.executor.plan.context.get(PLAN_CONTEXT_PROMPT, {}).pop(
PLAN_CONTEXT_PHONE, None
)
self.request.session.pop(SESSION_KEY_SMS_DEVICE, None)
self.executor.plan.context.pop(PLAN_CONTEXT_SMS_DEVICE, None)
self.logger.warning("failed to send SMS message to pre-set number", exc=exc)
return self.get(request, *args, **kwargs)
return super().get(request, *args, **kwargs)
def challenge_valid(self, response: ChallengeResponse) -> HttpResponse:
"""SMS Token is validated by challenge"""
device: SMSDevice = self.request.session[SESSION_KEY_SMS_DEVICE]
device: SMSDevice = self.executor.plan.context[PLAN_CONTEXT_SMS_DEVICE]
if not device.confirmed:
return self.challenge_invalid(response)
stage: AuthenticatorSMSStage = self.executor.current_stage
@@ -135,5 +135,5 @@ class AuthenticatorSMSStageView(ChallengeStageView):
self.logger.debug("Hashing number on device")
device.set_hashed_number()
device.save()
del self.request.session[SESSION_KEY_SMS_DEVICE]
del self.executor.plan.context[PLAN_CONTEXT_SMS_DEVICE]
return self.executor.stage_ok()

View File

@@ -18,7 +18,7 @@ from authentik.stages.authenticator_sms.models import (
SMSProviders,
hash_phone_number,
)
from authentik.stages.authenticator_sms.stage import PLAN_CONTEXT_PHONE, SESSION_KEY_SMS_DEVICE
from authentik.stages.authenticator_sms.stage import PLAN_CONTEXT_PHONE, PLAN_CONTEXT_SMS_DEVICE
from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
@@ -125,7 +125,7 @@ class AuthenticatorSMSStageTests(FlowTestCase):
self.assertEqual(mocker.call_count, 1)
self.assertEqual(mocker.request_history[0].method, "POST")
request_body = dict(parse_qsl(mocker.request_history[0].body))
device: SMSDevice = self.client.session[SESSION_KEY_SMS_DEVICE]
device: SMSDevice = self.get_flow_plan().context[PLAN_CONTEXT_SMS_DEVICE]
self.assertEqual(
request_body,
{

View File

@@ -5,7 +5,7 @@ from urllib.parse import quote
from django.http import HttpRequest, HttpResponse
from django.http.request import QueryDict
from django.utils.translation import gettext_lazy as _
from rest_framework.fields import CharField, IntegerField
from rest_framework.fields import CharField
from rest_framework.serializers import ValidationError
from authentik.flows.challenge import (
@@ -32,10 +32,10 @@ class AuthenticatorTOTPChallengeResponse(ChallengeResponse):
device: TOTPDevice
code = IntegerField()
code = CharField()
component = CharField(default="ak-stage-authenticator-totp")
def validate_code(self, code: int) -> int:
def validate_code(self, code: str) -> str:
"""Validate totp code"""
if not self.device:
raise ValidationError(_("Code does not match"))

View File

@@ -1,5 +1,6 @@
"""authentik consent stage"""
from hmac import compare_digest
from uuid import uuid4
from django.http import HttpRequest, HttpResponse
@@ -23,7 +24,7 @@ PLAN_CONTEXT_CONSENT = "consent"
PLAN_CONTEXT_CONSENT_HEADER = "consent_header"
PLAN_CONTEXT_CONSENT_PERMISSIONS = "consent_permissions"
PLAN_CONTEXT_CONSENT_EXTRA_PERMISSIONS = "consent_additional_permissions"
SESSION_KEY_CONSENT_TOKEN = "authentik/stages/consent/token" # nosec
PLAN_CONTEXT_CONSENT_TOKEN = "goauthentik.io/stages/consent/token" # nosec
class ConsentPermissionSerializer(PassiveSerializer):
@@ -50,7 +51,9 @@ class ConsentChallengeResponse(ChallengeResponse):
token = CharField(required=True)
def validate_token(self, token: str):
if token != self.stage.executor.request.session[SESSION_KEY_CONSENT_TOKEN]:
if not compare_digest(
token, self.stage.executor.plan.context.get(PLAN_CONTEXT_CONSENT_TOKEN, "")
):
raise ValidationError(_("Invalid consent token, re-showing prompt"))
return token
@@ -62,7 +65,7 @@ class ConsentStageView(ChallengeStageView):
def get_challenge(self) -> Challenge:
token = str(uuid4())
self.request.session[SESSION_KEY_CONSENT_TOKEN] = token
self.executor.plan.context[PLAN_CONTEXT_CONSENT_TOKEN] = token
data = {
"permissions": self.executor.plan.context.get(PLAN_CONTEXT_CONSENT_PERMISSIONS, []),
"additional_permissions": self.executor.plan.context.get(

View File

@@ -19,7 +19,7 @@ from authentik.stages.consent.models import ConsentMode, ConsentStage, UserConse
from authentik.stages.consent.stage import (
PLAN_CONTEXT_CONSENT_HEADER,
PLAN_CONTEXT_CONSENT_PERMISSIONS,
SESSION_KEY_CONSENT_TOKEN,
PLAN_CONTEXT_CONSENT_TOKEN,
)
@@ -83,11 +83,10 @@ class TestConsentStage(FlowTestCase):
)
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": session[SESSION_KEY_CONSENT_TOKEN],
"token": self.get_flow_plan().context[PLAN_CONTEXT_CONSENT_TOKEN],
},
)
@@ -122,7 +121,7 @@ class TestConsentStage(FlowTestCase):
response = self.client.post(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
{
"token": session[SESSION_KEY_CONSENT_TOKEN],
"token": self.get_flow_plan().context[PLAN_CONTEXT_CONSENT_TOKEN],
},
)
self.assertEqual(response.status_code, 200)

View File

@@ -19,7 +19,7 @@ from authentik.flows.tests import FlowTestCase
from authentik.flows.views.executor import QS_KEY_TOKEN, SESSION_KEY_PLAN, FlowExecutorView
from authentik.lib.config import CONFIG
from authentik.lib.generators import generate_id
from authentik.stages.consent.stage import SESSION_KEY_CONSENT_TOKEN
from authentik.stages.consent.stage import PLAN_CONTEXT_CONSENT_TOKEN
from authentik.stages.email.models import EmailStage
from authentik.stages.email.stage import PLAN_CONTEXT_EMAIL_OVERRIDE, EmailStageView
@@ -174,7 +174,7 @@ class TestEmailStage(FlowTestCase):
kwargs={"flow_slug": self.flow.slug},
),
data={
"token": self.client.session[SESSION_KEY_CONSENT_TOKEN],
"token": self.get_flow_plan().context[PLAN_CONTEXT_CONSENT_TOKEN],
},
follow=True,
)

View File

@@ -245,7 +245,10 @@ class WorkerStatusMiddleware(Middleware):
WorkerStatusMiddleware.keep(status)
except DB_ERRORS: # pragma: no cover
sleep(10)
pass
try:
connections.close_all()
except DB_ERRORS:
pass
@staticmethod
def keep(status: WorkerStatus):

View File

@@ -37,7 +37,7 @@ services:
- ${COMPOSE_PORT_HTTPS:-9443}:9443
restart: unless-stopped
volumes:
- ./media:/data/media
- ./data:/data
- ./custom-templates:/templates
worker:
command: worker
@@ -57,7 +57,7 @@ services:
user: root
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./media:/data/media
- ./data:/data
- ./certs:/certs
- ./custom-templates:/templates
volumes:

2
go.mod
View File

@@ -32,7 +32,7 @@ require (
github.com/spf13/cobra v1.10.2
github.com/stretchr/testify v1.11.1
github.com/wwt/guac v1.3.2
goauthentik.io/api/v3 v3.2026020.1
goauthentik.io/api/v3 v3.2026020.3
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
golang.org/x/oauth2 v0.34.0
golang.org/x/sync v0.19.0

4
go.sum
View File

@@ -214,8 +214,8 @@ go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
goauthentik.io/api/v3 v3.2026020.1 h1:R7WdvVmfm066d3Zu7R+WfjDGdFqC/X2gONHIGPfcLzk=
goauthentik.io/api/v3 v3.2026020.1/go.mod h1:82lqAz4jxzl6Cg0YDbhNtvvTG2rm6605ZhdJFnbbsl8=
goauthentik.io/api/v3 v3.2026020.3 h1:CKtPyAQToPT2yF5odTTc+IfPLhYeVX9FbLMeVnFgZps=
goauthentik.io/api/v3 v3.2026020.3/go.mod h1:82lqAz4jxzl6Cg0YDbhNtvvTG2rm6605ZhdJFnbbsl8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=

View File

@@ -1,7 +1,7 @@
# syntax=docker/dockerfile:1
# Stage 1: Build
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.25.5-trixie@sha256:5d35fb8d28b9095d123b7d96095bbf3750ff18be0a87e5a21c9cffc4351fbf96 AS builder
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.25.5-trixie@sha256:8e8f9c84609b6005af0a4a8227cee53d6226aab1c6dcb22daf5aeeb8b05480e1 AS builder
ARG TARGETOS
ARG TARGETARCH
@@ -31,7 +31,7 @@ RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \
go build -o /go/ldap ./cmd/ldap
# Stage 2: Run
FROM ghcr.io/goauthentik/fips-debian:trixie-slim-fips@sha256:2f19fc114923ec0842329bf638cb155e597c4be9c8119a3db038ffc3fede9228
FROM ghcr.io/goauthentik/fips-debian:trixie-slim-fips@sha256:10dadf1df1337e8eb4218acef6a3027abebf0a155a95d792af736f85ab0e9588
ARG VERSION
ARG GIT_BUILD_HASH

View File

@@ -9,7 +9,7 @@
"version": "0.0.0",
"license": "MIT",
"devDependencies": {
"aws-cdk": "^2.1034.0",
"aws-cdk": "^2.1100.1",
"cross-env": "^10.1.0"
},
"engines": {
@@ -25,9 +25,9 @@
"license": "MIT"
},
"node_modules/aws-cdk": {
"version": "2.1034.0",
"resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1034.0.tgz",
"integrity": "sha512-YsIeXmMP/9eGml/eoPs64kHzNR0IVezzwuH0XrLOtUCjYNb80cmmjoCNsMn96u9rJOte1Yg3jitrHi1wTqXAqw==",
"version": "2.1100.1",
"resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1100.1.tgz",
"integrity": "sha512-q2poFrQh90TK6eqeI0zznA8r1JkDI63WVOSqC7gFGo6qjQjAnvFk/utxHoNRgAC0RL0CLd19uCcHh3jfX9NiSg==",
"dev": true,
"license": "Apache-2.0",
"bin": {

View File

@@ -7,7 +7,7 @@
"aws-cfn": "cross-env CI=false cdk synth --version-reporting=false > template.yaml"
},
"devDependencies": {
"aws-cdk": "^2.1034.0",
"aws-cdk": "^2.1100.1",
"cross-env": "^10.1.0"
},
"engines": {

Binary file not shown.

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-12-12 15:51+0000\n"
"POT-Creation-Date: 2025-12-17 00:10+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -558,6 +558,30 @@ msgid ""
"encryption."
msgstr ""
#: authentik/crypto/models.py
msgid "Key algorithm type detected from the certificate's public key"
msgstr ""
#: authentik/crypto/models.py
msgid "Certificate expiry date"
msgstr ""
#: authentik/crypto/models.py
msgid "Certificate subject as RFC4514 string"
msgstr ""
#: authentik/crypto/models.py
msgid "SHA256 fingerprint of the certificate"
msgstr ""
#: authentik/crypto/models.py
msgid "SHA1 fingerprint of the certificate"
msgstr ""
#: authentik/crypto/models.py
msgid "Key ID generated from private key"
msgstr ""
#: authentik/crypto/models.py
msgid "Certificate-Key Pair"
msgstr ""

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2017 ivelum
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,3 +0,0 @@
This is a fork of djangoql.
https://github.com/ivelum/djangoql

View File

@@ -1 +0,0 @@
__version__ = "0.18.1"

View File

@@ -1,91 +0,0 @@
from typing import TYPE_CHECKING, Any
if TYPE_CHECKING:
from akql.parser import AKQLParser
class Node:
def __str__(self):
children = []
for k, v in self.__dict__.items():
vv = v
if isinstance(v, list | tuple):
vv = "[{}]".format(", ".join([str(v) for v in v if v]))
children.append(f"{k}={vv}")
return "<{}{}{}>".format(
self.__class__.__name__,
": " if children else "",
", ".join(children),
)
__repr__ = __str__
def __eq__(self, other):
if not isinstance(other, self.__class__):
return False
for k, v in self.__dict__.items():
if getattr(other, k) != v:
return False
return True
def __ne__(self, other):
return not self.__eq__(other)
class Expression(Node):
def __init__(self, left, operator, right):
self.left = left
self.operator = operator
self.right = right
class Name(Node):
def __init__(self, parts):
if isinstance(parts, list):
self.parts = parts
elif isinstance(parts, tuple):
self.parts = list(parts)
else:
self.parts = [parts]
@property
def value(self):
return ".".join(self.parts)
class Const(Node):
def __init__(self, value):
self.value = value
class List(Node):
def __init__(self, items):
self.items = items
@property
def value(self):
return [i.value for i in self.items]
class Operator(Node):
def __init__(self, operator):
self.operator = operator
class Logical(Operator):
pass
class Comparison(Operator):
pass
class Variable(Node):
def __init__(self, name: str, parser: "AKQLParser"):
self.name = name
self.parser = parser
@property
def value(self) -> Any:
return self.parser.context.get(self.name)

View File

@@ -1,32 +0,0 @@
class AKQLError(Exception):
def __init__(self, message=None, value=None, line=None, column=None):
self.value = value
self.line = line
self.column = column
super().__init__(message)
def __str__(self):
message = super().__str__()
if self.line:
position_info = f"Line {self.line}"
if self.column:
position_info += f", col {self.column}"
return f"{position_info}: {message}"
else:
return message
class AKQLSyntaxError(AKQLError):
pass
class AKQLLexerError(AKQLSyntaxError):
pass
class AKQLParserError(AKQLSyntaxError):
pass
class AKQLSchemaError(AKQLError):
pass

View File

@@ -1,181 +0,0 @@
from ply import lex
from ply.lex import TOKEN, Lexer, LexToken
from akql.exceptions import AKQLLexerError
class AKQLLexer:
_lexer: Lexer
def __init__(self, **kwargs):
self._lexer = lex.lex(module=self, **kwargs)
self.reset()
def reset(self):
self.text = ""
self._lexer.lineno = 1
return self
def input(self, s):
self.reset()
self.text = s
self._lexer.input(s)
return self
def token(self):
return self._lexer.token()
# Iterator interface
def __iter__(self):
return self
def next(self):
t = self.token()
if t is None:
raise StopIteration
return t
__next__ = next
def find_column(self, t: LexToken):
"""
Returns token position in current text, starting from 1
"""
cr = max(self.text.rfind(lt, 0, t.lexpos) for lt in self.line_terminators)
if cr == -1:
return t.lexpos + 1
return t.lexpos - cr
whitespace = " \t\v\f\u00a0"
line_terminators = "\n\r\u2028\u2029"
re_line_terminators = r"\n\r\u2028\u2029"
re_escaped_char = r"\\[\"\\/bfnrt]"
re_escaped_unicode = r"\\u[0-9A-Fa-f]{4}"
re_string_char = r"[^\"\\" + re_line_terminators + "]"
re_int_value = r"(-?0|-?[1-9][0-9]*)"
re_fraction_part = r"\.[0-9]+"
re_exponent_part = r"[eE][\+-]?[0-9]+"
tokens = [
"COMMA",
"OR",
"AND",
"NOT",
"IN",
"TRUE",
"FALSE",
"NONE",
"NAME",
"STRING_VALUE",
"FLOAT_VALUE",
"INT_VALUE",
"PAREN_L",
"PAREN_R",
"EQUALS",
"NOT_EQUALS",
"GREATER",
"GREATER_EQUAL",
"LESS",
"LESS_EQUAL",
"CONTAINS",
"NOT_CONTAINS",
"STARTSWITH",
"ENDSWITH",
"VARIABLE",
]
t_COMMA = ","
t_PAREN_L = r"\("
t_PAREN_R = r"\)"
t_EQUALS = "="
t_NOT_EQUALS = "!="
t_GREATER = ">"
t_GREATER_EQUAL = ">="
t_LESS = "<"
t_LESS_EQUAL = "<="
t_CONTAINS = "~"
t_NOT_CONTAINS = "!~"
t_NAME = r"[_A-Za-z][_0-9A-Za-z]*(\.[_A-Za-z][_0-9A-Za-z]*)*"
t_ignore = whitespace
@TOKEN(r"\$([_A-Za-z\.]+)")
def t_VARIABLE(self, t: LexToken):
return t
@TOKEN(r"\"(" + re_escaped_char + "|" + re_escaped_unicode + "|" + re_string_char + r")*\"")
def t_STRING_VALUE(self, t: LexToken):
t.value = t.value[1:-1] # cut leading and trailing quotes ""
return t
@TOKEN(
re_int_value
+ re_fraction_part
+ re_exponent_part
+ "|"
+ re_int_value
+ re_fraction_part
+ "|"
+ re_int_value
+ re_exponent_part
)
def t_FLOAT_VALUE(self, t: LexToken):
return t
@TOKEN(re_int_value)
def t_INT_VALUE(self, t: LexToken):
return t
not_followed_by_name = "(?![_0-9A-Za-z])"
@TOKEN("or" + not_followed_by_name)
def t_OR(self, t: LexToken):
return t
@TOKEN("and" + not_followed_by_name)
def t_AND(self, t: LexToken):
return t
@TOKEN("not" + not_followed_by_name)
def t_NOT(self, t: LexToken):
return t
@TOKEN("in" + not_followed_by_name)
def t_IN(self, t: LexToken):
return t
@TOKEN("startswith" + not_followed_by_name)
def t_STARTSWITH(self, t: LexToken):
return t
@TOKEN("endswith" + not_followed_by_name)
def t_ENDSWITH(self, t: LexToken):
return t
@TOKEN("True" + not_followed_by_name)
def t_TRUE(self, t: LexToken):
return t
@TOKEN("False" + not_followed_by_name)
def t_FALSE(self, t: LexToken):
return t
@TOKEN("None" + not_followed_by_name)
def t_NONE(self, t: LexToken):
return t
def t_error(self, t: LexToken):
raise AKQLLexerError(
message=f"Illegal character {repr(t.value[0])}",
value=t.value,
line=t.lineno,
column=self.find_column(t),
)
@TOKEN("[" + re_line_terminators + "]+")
def t_newline(self, t: LexToken):
t.lexer.lineno += len(t.value)

View File

@@ -1,239 +0,0 @@
import re
from decimal import Decimal
from typing import Any
from ply import yacc
from ply.yacc import LRParser, YaccProduction
from akql.ast import Comparison, Const, Expression, List, Logical, Name, Variable
from akql.exceptions import AKQLParserError
from akql.lexer import AKQLLexer
unescape_pattern = re.compile(
"(" + AKQLLexer.re_escaped_char + "|" + AKQLLexer.re_escaped_unicode + ")",
)
def unescape_repl(m: re.Match[str]) -> str:
contents = m.group(1)
if len(contents) == 2: # noqa
return contents[1]
else:
return contents.encode("utf8").decode("unicode_escape")
def unescape(value):
if isinstance(value, bytes):
value = value.decode("utf8")
return re.sub(unescape_pattern, unescape_repl, value)
class AKQLParser:
yacc: LRParser
context: dict[str, Any]
def __init__(self, debug=False, context: dict[str, Any] | None = None, **kwargs):
self.default_lexer = AKQLLexer()
self.tokens = self.default_lexer.tokens
kwargs["debug"] = debug
if "write_tables" not in kwargs:
kwargs["write_tables"] = False
self.context = context or {}
self.yacc = yacc.yacc(module=self, **kwargs)
def parse(
self, input=None, lexer: AKQLLexer | None = None, **kwargs
) -> Expression: # noqa: A002
lexer = lexer or self.default_lexer
return self.yacc.parse(input=input, lexer=lexer, **kwargs)
start = "expression"
def p_expression_parens(self, p: YaccProduction):
"""
expression : PAREN_L expression PAREN_R
"""
p[0] = p[2]
def p_expression_logical(self, p: YaccProduction):
"""
expression : expression logical expression
"""
p[0] = Expression(left=p[1], operator=p[2], right=p[3])
def p_expression_comparison(self, p: YaccProduction):
"""
expression : name comparison_number number
| name comparison_string string
| name comparison_equality boolean_value
| name comparison_equality none
| name comparison_in_list const_list_value
| name comparison_number variable
| name comparison_string variable
| name comparison_equality variable
| name comparison_in_list variable
"""
p[0] = Expression(left=p[1], operator=p[2], right=p[3])
def p_name(self, p: YaccProduction):
"""
name : NAME
"""
p[0] = Name(parts=p[1].split("."))
def p_logical(self, p: YaccProduction):
"""
logical : AND
| OR
"""
p[0] = Logical(operator=p[1])
def p_comparison_number(self, p: YaccProduction):
"""
comparison_number : comparison_equality
| comparison_greater_less
"""
p[0] = p[1]
def p_comparison_string(self, p: YaccProduction):
"""
comparison_string : comparison_equality
| comparison_greater_less
| comparison_string_specific
"""
p[0] = p[1]
def p_comparison_equality(self, p: YaccProduction):
"""
comparison_equality : EQUALS
| NOT_EQUALS
"""
p[0] = Comparison(operator=p[1])
def p_comparison_greater_less(self, p: YaccProduction):
"""
comparison_greater_less : GREATER
| GREATER_EQUAL
| LESS
| LESS_EQUAL
"""
p[0] = Comparison(operator=p[1])
def p_comparison_string_specific(self, p: YaccProduction):
"""
comparison_string_specific : CONTAINS
| NOT_CONTAINS
| STARTSWITH
| NOT STARTSWITH
| ENDSWITH
| NOT ENDSWITH
"""
p[0] = Comparison(operator=" ".join(p[1:]))
def p_comparison_in_list(self, p: YaccProduction):
"""
comparison_in_list : IN
| NOT IN
"""
p[0] = Comparison(operator=" ".join(p[1:]))
def p_const_value(self, p: YaccProduction):
"""
const_value : number
| string
| none
| boolean_value
"""
p[0] = p[1]
def p_variable(self, p: YaccProduction):
"""
variable : VARIABLE
"""
p[0] = Variable(name=unescape(p[1]), parser=self)
def p_number_int(self, p: YaccProduction):
"""
number : INT_VALUE
"""
p[0] = Const(value=int(p[1]))
def p_number_float(self, p: YaccProduction):
"""
number : FLOAT_VALUE
"""
p[0] = Const(value=Decimal(p[1]))
def p_string(self, p: YaccProduction):
"""
string : STRING_VALUE
"""
p[0] = Const(value=unescape(p[1]))
def p_none(self, p: YaccProduction):
"""
none : NONE
"""
p[0] = Const(value=None)
def p_boolean_value(self, p: YaccProduction):
"""
boolean_value : true
| false
"""
p[0] = p[1]
def p_true(self, p: YaccProduction):
"""
true : TRUE
"""
p[0] = Const(value=True)
def p_false(self, p: YaccProduction):
"""
false : FALSE
"""
p[0] = Const(value=False)
def p_const_list_value(self, p: YaccProduction):
"""
const_list_value : PAREN_L const_value_list PAREN_R
"""
p[0] = List(items=p[2])
def p_const_value_list(self, p: YaccProduction):
"""
const_value_list : const_value_list COMMA const_value
"""
p[0] = p[1] + [p[3]]
def p_const_value_list_single(self, p: YaccProduction):
"""
const_value_list : const_value
"""
p[0] = [p[1]]
def p_error(self, token):
if token is None:
self.raise_syntax_error("Unexpected end of input")
else:
fragment = str(token.value)
self.raise_syntax_error(
f"Syntax error at {repr(fragment)}",
token=token,
)
def raise_syntax_error(self, message, token=None):
if token is None:
raise AKQLParserError(message)
lexer = token.lexer
if callable(getattr(lexer, "find_column", None)):
column = lexer.find_column(token)
else:
column = None
raise AKQLParserError(
message=message,
value=token.value,
line=token.lineno,
column=column,
)

File diff suppressed because it is too large Load Diff

View File

@@ -1,47 +0,0 @@
from typing import Any
from django.db.models import QuerySet
from akql.ast import Logical
from akql.parser import AKQLParser
from akql.schema import AKQLField, AKQLSchema
def build_filter(expr: str, schema_instance: AKQLSchema):
if isinstance(expr.operator, Logical):
left = build_filter(expr.left, schema_instance)
right = build_filter(expr.right, schema_instance)
if expr.operator.operator == "or":
return left | right
else:
return left & right
field = schema_instance.resolve_name(expr.left)
if not field:
# That must be a reference to a model without specifying a field.
# Let's construct an abstract lookup field for it
field = AKQLField(
name=expr.left.parts[-1],
nullable=True,
)
return field.get_lookup(
path=expr.left.parts[:-1],
operator=expr.operator.operator,
value=expr.right.value,
)
def apply_search(
queryset: QuerySet,
search: str,
context: dict[str, Any] | None = None,
schema: type[AKQLSchema] | None = None,
) -> QuerySet:
"""
Applies search written in DjangoQL mini-language to given queryset
"""
ast = AKQLParser(context=context).parse(search)
schema = schema or AKQLSchema
schema_instance = schema(queryset.model)
schema_instance.validate(ast)
return queryset.filter(build_filter(ast, schema_instance))

View File

@@ -1,618 +0,0 @@
import inspect
import warnings
from collections import OrderedDict, defaultdict, deque
from collections.abc import Generator
from datetime import datetime
from decimal import Decimal
from django.conf import settings
from django.core.exceptions import FieldDoesNotExist
from django.db import connection, models
from django.db.models import ManyToManyRel, ManyToOneRel, Model, Q
from django.db.models.fields.related import ForeignObjectRel
from django.utils.timezone import get_current_timezone
from akql.ast import Comparison, Const, List, Logical, Name, Node, Variable
from akql.exceptions import AKQLSchemaError
class AKQLField:
"""
Abstract searchable field
"""
model = None
name = None
nullable = False
suggest_options = False
type = "unknown"
value_types = []
value_types_description = ""
def __init__(self, model=None, name=None, nullable=None, suggest_options=None):
if model is not None:
self.model = model
if name is not None:
self.name = name
if nullable is not None:
self.nullable = nullable
if suggest_options is not None:
self.suggest_options = suggest_options
def _field_choices(self):
if self.model:
try:
return self.model._meta.get_field(self.name).choices
except (AttributeError, FieldDoesNotExist):
pass
return []
@property
def async_options(self):
return not self._field_choices()
def get_options(self, search):
"""
Override this method to provide custom suggestion options
"""
result = []
choices = self._field_choices()
if choices:
search = search.lower()
for c in choices:
choice = str(c[1])
if search in choice.lower():
result.append(choice)
return result
def get_lookup_name(self):
"""
Override this method to provide custom lookup name
"""
return self.name
def get_lookup_value(self, value):
"""
Override this method to convert displayed values to lookup values
"""
choices = self._field_choices()
if choices:
if isinstance(value, list):
return [c[0] for c in choices if c[0] in value or c[1] in value]
else:
for c in choices:
if value in c:
return c[0]
return value
def get_operator(self, operator):
"""
Get a comparison suffix to be used in Django ORM & inversion flag for it
:param operator: string, DjangoQL comparison operator
:return: (suffix, invert) - a tuple with 2 values:
suffix - suffix to be used in ORM query, for example '__gt' for '>'
invert - boolean, True if this comparison needs to be inverted
"""
op = {
"=": "",
">": "__gt",
">=": "__gte",
"<": "__lt",
"<=": "__lte",
"~": "__icontains",
"in": "__in",
"startswith": "__istartswith",
"endswith": "__iendswith",
}.get(operator)
if op is not None:
return op, False
op = {
"!=": "",
"!~": "__icontains",
"not in": "__in",
"not startswith": "__istartswith",
"not endswith": "__iendswith",
}[operator]
return op, True
def get_lookup(self, path, operator, value):
"""
Performs a lookup for this field with given path, operator and value.
Override this if you'd like to implement a fully custom lookup. It
should support all comparison operators compatible with the field type.
:param path: a list of names preceding current lookup. For example,
if expression looks like 'author.groups.name = "Foo"' path would
be ['author', 'groups']. 'name' is not included, because it's the
current field instance itself.
:param operator: a string with comparison operator. It could be one of
the following: '=', '!=', '>', '>=', '<', '<=', '~', '!~', 'in',
'not in'. Depending on the field type, some operators may be
excluded. '~' and '!~' can be applied to StrField only and aren't
allowed for any other fields. BoolField can't be used with less or
greater operators, '>', '>=', '<' and '<=' are excluded for it.
:param value: value passed for comparison
:return: Q-object
"""
search = "__".join(path + [self.get_lookup_name()])
op, invert = self.get_operator(operator)
q = models.Q(**{f"{search}{op}": self.get_lookup_value(value)})
return ~q if invert else q
def validate(self, value):
if not self.nullable and value is None:
raise AKQLSchemaError(
f"Field {self.name} is not nullable, " "can't compare it to None",
)
if value is not None and type(value) not in self.value_types:
if self.nullable:
msg = (
'Field "{field}" has "nullable {field_type}" type. '
"It can be compared to {possible_values} or None, "
"but not to {value}"
)
else:
msg = (
'Field "{field}" has "{field_type}" type. It can '
"be compared to {possible_values}, "
"but not to {value}"
)
raise AKQLSchemaError(
msg.format(
field=self.name,
field_type=self.type,
possible_values=self.value_types_description,
value=repr(value),
)
)
class IntField(AKQLField):
type = "int"
value_types = [int]
value_types_description = "integer numbers"
def validate(self, value):
"""
Support enum-like choices defined on an integer field
"""
return super().validate(self.get_lookup_value(value))
class FloatField(AKQLField):
type = "float"
value_types = [int, float, Decimal]
value_types_description = "floating point numbers"
class StrField(AKQLField):
type = "str"
value_types = [str]
value_types_description = "strings"
def get_options(self, search):
choice_options = super().get_options(search)
if choice_options:
return choice_options
lookup = {}
if search:
lookup[f"{self.name}__icontains"] = search
return (
self.model.objects.filter(**lookup)
.order_by(self.name)
.values_list(self.name, flat=True)
.distinct()
)
class BoolField(AKQLField):
type = "bool"
value_types = [bool]
value_types_description = "True or False"
class DateField(AKQLField):
type = "date"
value_types = [str]
value_types_description = 'dates in "YYYY-MM-DD" format'
def validate(self, value):
super().validate(value)
try:
self.get_lookup_value(value)
except ValueError as exc:
raise AKQLSchemaError(
f'Field "{self.name}" can be compared to dates in '
f'"YYYY-MM-DD" format, but not to {repr(value)}',
) from exc
def get_lookup_value(self, value):
if not value:
return None
return datetime.strptime(value, "%Y-%m-%d").date()
class DateTimeField(AKQLField):
type = "datetime"
value_types = [str]
value_types_description = 'timestamps in "YYYY-MM-DD HH:MM" format'
def validate(self, value):
super().validate(value)
try:
self.get_lookup_value(value)
except ValueError as exc:
raise AKQLSchemaError(
f'Field "{self.name}" can be compared to timestamps in '
f'"YYYY-MM-DD HH:MM" format, but not to {repr(value)}',
) from exc
def get_lookup_value(self, value):
if not value:
return None
for format in [
"%Y-%m-%d",
"%Y-%m-%d %H:%M",
"%Y-%m-%d %H:%M:%S",
]:
try:
dt = datetime.strptime(value, format)
if settings.USE_TZ:
dt = dt.replace(tzinfo=get_current_timezone())
return dt
except ValueError:
pass
return None
def get_lookup(self, path, operator, value):
search = "__".join(path + [self.get_lookup_name()])
op, invert = self.get_operator(operator)
# Add LIKE operator support for datetime fields. For LIKE comparisons
# we don't want to convert source value to datetime instance, because
# it would effectively kill the idea. What we want is expressions like
# 'created ~ "2017-01-30'
# to be translated to
# 'created LIKE %2017-01-30%',
# but it would work only if we pass a string as a parameter. If we pass
# a datetime instance, it would add time part in a form of 00:00:00,
# and resulting comparison would look like
# 'created LIKE %2017-01-30 00:00:00%'
# which is not what we want for this case.
val = value if operator in ("~", "!~") else self.get_lookup_value(value)
q = models.Q(**{f"{search}{op}": val})
return ~q if invert else q
class RelationField(AKQLField):
type = "relation"
def __init__(self, model, name, related_model, nullable=False, suggest_options=False):
super().__init__(
model=model,
name=name,
nullable=nullable,
suggest_options=suggest_options,
)
self.related_model = related_model
@property
def relation(self):
return AKQLSchema.model_label(self.related_model)
class JSONSearchField(StrField):
"""JSON field for DjangoQL"""
model: Model
def __init__(self, model=None, name=None, nullable=None, suggest_nested=True):
# Set this in the constructor to not clobber the type variable
self.type = "relation"
self.suggest_nested = suggest_nested
super().__init__(model, name, nullable)
def get_lookup(self, path, operator, value):
search = "__".join(path)
op, invert = self.get_operator(operator)
q = Q(**{f"{search}{op}": self.get_lookup_value(value)})
return ~q if invert else q
def json_field_keys(self) -> Generator[tuple[str]]:
with connection.cursor() as cursor:
cursor.execute(
f"""
WITH RECURSIVE "{self.name}_keys" AS (
SELECT
ARRAY[jsonb_object_keys("{self.name}")] AS key_path_array,
"{self.name}" -> jsonb_object_keys("{self.name}") AS value
FROM {self.model._meta.db_table}
WHERE "{self.name}" IS NOT NULL
AND jsonb_typeof("{self.name}") = 'object'
UNION ALL
SELECT
ck.key_path_array || jsonb_object_keys(ck.value),
ck.value -> jsonb_object_keys(ck.value) AS value
FROM "{self.name}_keys" ck
WHERE jsonb_typeof(ck.value) = 'object'
),
unique_paths AS (
SELECT DISTINCT key_path_array
FROM "{self.name}_keys"
)
SELECT key_path_array FROM unique_paths;
""" # nosec
)
return (x[0] for x in cursor.fetchall())
def get_nested_options(self) -> OrderedDict:
"""Get keys of all nested objects to show autocomplete"""
if not self.suggest_nested:
return OrderedDict()
base_model_name = f"{self.model._meta.app_label}.{self.model._meta.model_name}_{self.name}"
def recursive_function(parts: list[str], parent_parts: list[str] | None = None):
if not parent_parts:
parent_parts = []
path = parts.pop(0)
parent_parts.append(path)
relation_key = "_".join(parent_parts)
if len(parts) > 1:
out_dict = {
relation_key: {
parts[0]: {
"type": "relation",
"relation": f"{relation_key}_{parts[0]}",
}
}
}
child_paths = recursive_function(parts.copy(), parent_parts.copy())
child_paths.update(out_dict)
return child_paths
else:
return {relation_key: {parts[0]: {}}}
relation_structure = defaultdict(dict)
for relations in self.json_field_keys():
result = recursive_function([base_model_name] + relations)
for relation_key, value in result.items():
for sub_relation_key, sub_value in value.items():
if not relation_structure[relation_key].get(sub_relation_key, None):
relation_structure[relation_key][sub_relation_key] = sub_value
else:
relation_structure[relation_key][sub_relation_key].update(sub_value)
final_dict = defaultdict(dict)
for key, value in relation_structure.items():
for sub_key, sub_value in value.items():
if not sub_value:
final_dict[key][sub_key] = {
"type": "str",
"nullable": True,
}
else:
final_dict[key][sub_key] = sub_value
return OrderedDict(final_dict)
def relation(self) -> str:
return f"{self.model._meta.app_label}.{self.model._meta.model_name}_{self.name}"
class ChoiceSearchField(StrField):
def __init__(self, model=None, name=None, nullable=None):
super().__init__(model, name, nullable, suggest_options=True)
def get_options(self, search):
result = []
choices = self._field_choices()
if choices:
search = search.lower()
for c in choices:
choice = str(c[0])
if search in choice.lower():
result.append(choice)
return result
class AKQLSchema:
include = () # models to include into introspection
exclude = () # models to exclude from introspection
suggest_options = None
def __init__(self, model):
if not inspect.isclass(model) or not issubclass(model, models.Model):
raise AKQLSchemaError(
"Schema must be initialized with a subclass of Django model",
)
if self.include and self.exclude:
raise AKQLSchemaError(
"Either include or exclude can be specified, but not both",
)
if self.excluded(model):
raise AKQLSchemaError(
f"{model} can't be used with {self.__class__} because it's excluded from it",
)
self.current_model = model
self._models = None
if self.suggest_options is None:
self.suggest_options = {}
def excluded(self, model):
return model in self.exclude or (self.include and model not in self.include)
@property
def models(self):
if not self._models:
self._models = self.introspect(
model=self.current_model,
exclude=tuple(self.model_label(m) for m in self.exclude),
)
return self._models
@classmethod
def model_label(self, model):
return str(model._meta)
def introspect(self, model, exclude=()):
"""
Start with given model and recursively walk through its relationships.
Returns a dict with all model labels and their fields found.
"""
result = {}
open_set = deque([model])
closed_set = set(exclude)
while open_set:
model = open_set.popleft()
model_label = self.model_label(model)
if model_label in closed_set:
continue
model_fields = OrderedDict()
for field in self.get_fields(model):
field_instance = field
if not isinstance(field, AKQLField):
field_instance = self.get_field_instance(model, field)
if not field_instance:
continue
if isinstance(field_instance, RelationField):
open_set.append(field_instance.related_model)
model_fields[field_instance.name] = field_instance
result[model_label] = model_fields
closed_set.add(model_label)
return result
def get_fields(self, model):
"""
By default, returns all field names of a given model.
Override this method to limit field options. You can either return a
plain list of field names from it, like ['id', 'name'], or call
.super() and exclude unwanted fields from its result.
"""
return sorted(
[f.name for f in model._meta.get_fields() if f.name != "password"],
)
def get_field_instance(self, model, field_name):
field = model._meta.get_field(field_name)
field_kwargs = {"model": model, "name": field.name}
if field.is_relation:
if not field.related_model:
# GenericForeignKey
return
if self.excluded(field.related_model):
return
field_cls = RelationField
field_kwargs["related_model"] = field.related_model
else:
field_cls = self.get_field_cls(field)
if isinstance(field, ManyToOneRel | ManyToManyRel | ForeignObjectRel):
# Django 1.8 doesn't have .null attribute for these fields
field_kwargs["nullable"] = True
else:
field_kwargs["nullable"] = field.null
field_kwargs["suggest_options"] = field.name in self.suggest_options.get(model, [])
return field_cls(**field_kwargs)
def get_field_cls(self, field):
str_fields = (
models.CharField,
models.TextField,
models.UUIDField,
models.BinaryField,
models.GenericIPAddressField,
)
if isinstance(field, str_fields):
return StrField
elif isinstance(field, models.AutoField | models.IntegerField):
return IntField
elif isinstance(field, models.BooleanField | models.NullBooleanField):
return BoolField
elif isinstance(field, models.DecimalField | models.FloatField):
return FloatField
elif isinstance(field, models.DateTimeField):
return DateTimeField
elif isinstance(field, models.DateField):
return DateField
return AKQLField
def as_dict(self):
from akql.serializers import AKQLSchemaSerializer
warnings.warn(
"DjangoQLSchema.as_dict() is deprecated and will be removed in "
"future releases. Please use DjangoQLSchemaSerializer instead.",
stacklevel=2,
)
return AKQLSchemaSerializer().serialize(self)
def resolve_name(self, name):
assert isinstance(name, Name)
model = self.model_label(self.current_model)
root_field = name.parts[0]
field = self.models[model].get(root_field)
# If the query goes into a JSON field, return the root
# field as the JSON field will do the rest
if isinstance(field, JSONSearchField):
# This is a workaround; build_filter will remove the right-most
# entry in the path as that is intended to be the same as the field
# however for JSON that is not the case
if name.parts[-1] != root_field:
name.parts.append(root_field)
return field
for name_part in name.parts:
field = self.models[model].get(name_part)
if not field:
raise AKQLSchemaError(
"Unknown field: {}. Possible choices are: {}".format(
name_part,
", ".join(sorted(self.models[model].keys())),
),
)
if field.type == "relation":
model = field.relation
field = None
return field
def validate(self, node):
"""
Validate DjangoQL AST tree vs. current schema
"""
assert isinstance(node, Node)
if isinstance(node.operator, Logical):
self.validate(node.left)
self.validate(node.right)
return
assert isinstance(node.left, Name)
assert isinstance(node.operator, Comparison)
assert isinstance(node.right, Const | List | Variable)
# Check that field and value types are compatible
field = self.resolve_name(node.left)
value = node.right.value
if field is None:
if value is not None:
raise AKQLSchemaError(
f"Related model {node.left.value} can be compared to None only, but not to "
f"{type(value).__name__}",
)
else:
values = value if isinstance(node.right, List) else [value]
for v in values:
field.validate(v)

View File

@@ -1,31 +0,0 @@
from collections import OrderedDict
from akql.schema import JSONSearchField, RelationField
class AKQLSchemaSerializer:
def serialize(self, schema):
models = {}
for model_label, fields in schema.models.items():
models[model_label] = OrderedDict(
[(name, self.serialize_field(f)) for name, f in fields.items()],
)
return {
"current_model": schema.model_label(schema.current_model),
"models": models,
}
def serialize_field(self, field):
result = {
"type": field.type,
"nullable": field.nullable,
"options": self.serialize_field_options(field),
}
if isinstance(field, RelationField):
result["relation"] = field.relation
if isinstance(field, JSONSearchField):
result["relation"] = field.relation()
return result
def serialize_field_options(self, field):
return list(field.get_options("")) if field.suggest_options else None

View File

@@ -1,16 +0,0 @@
from django.test import TestCase
from akql.queryset import apply_search
from authentik.core.tests.utils import create_test_user
from authentik.events.models import Notification
class TestFilter(TestCase):
def test_filter(self):
user = create_test_user()
notif = Notification.objects.create(user=user)
qs = apply_search(
Notification.objects.all(), "user.id = $current_user", {"$current_user": user.pk}
)
self.assertEqual(qs.first(), notif)

View File

@@ -1,18 +0,0 @@
from django.test import TestCase
from akql.lexer import AKQLLexer
class TestLexer(TestCase):
def test_lexer_simple(self):
lexer = AKQLLexer().input('foo = "bar"')
tokens = list(str(t) for t in lexer)
self.assertEqual(
tokens,
[
"LexToken(NAME,'foo',1,0)",
"LexToken(EQUALS,'=',1,4)",
"LexToken(STRING_VALUE,'bar',1,6)",
],
)

View File

@@ -1,41 +0,0 @@
from django.test import TestCase
from akql.ast import Comparison, Const, Expression, Name, Variable
from akql.parser import AKQLParser
class TestParser(TestCase):
def test_parser_simple(self):
ast = AKQLParser().parse('foo = "bar"')
self.assertEqual(
ast,
Expression(
left=Name(parts=["foo"]),
operator=Comparison(operator="="),
right=Const(value="bar"),
),
)
def test_parser_not_startswith(self):
ast = AKQLParser().parse('foo not startswith "bar"')
self.assertEqual(
ast,
Expression(
left=Name(parts=["foo"]),
operator=Comparison(operator="not startswith"),
right=Const(value="bar"),
),
)
def test_parser_variable(self):
parser = AKQLParser()
ast = parser.parse("foo = $bar")
self.assertEqual(
ast,
Expression(
left=Name(parts=["foo"]),
operator=Comparison(operator="="),
right=Variable(name="$bar", parser=parser),
),
)

View File

@@ -1,51 +0,0 @@
[project]
name = "akql"
version = "3.2.0"
description = "Model and object permissions for Django"
requires-python = ">=3.9,<3.14"
readme = "README.md"
license = { text = "MIT" }
authors = [
{ name = "Authentik Security Inc.", email = "hello@goauthentik.io" },
{ name = "Denis Stebunov", email = "support@ivelum.com" },
]
keywords = ["django", "permissions", "authorization", "object", "row", "level"]
classifiers = [
'Development Status :: 4 - Beta',
'Intended Audience :: Developers',
'Natural Language :: English',
'License :: OSI Approved :: MIT License',
'Programming Language :: Python',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11',
'Programming Language :: Python :: 3.12',
'Programming Language :: Python :: 3.13',
]
dependencies = [
"ply>=3.8",
]
[project.urls]
Homepage = "https://github.com/goauthentik/authentik/tree/main/packages/akql"
Documentation = "https://github.com/goauthentik/authentik/tree/main/packages/akql"
Repository = "https://github.com/goauthentik/authentik/tree/main/packages/akql"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = [
"akql",
]
[tool.setuptools.packages]
find = {}

View File

@@ -17,7 +17,7 @@ COPY web .
RUN npm run build-proxy
# Stage 2: Build
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.25.5-trixie@sha256:5d35fb8d28b9095d123b7d96095bbf3750ff18be0a87e5a21c9cffc4351fbf96 AS builder
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.25.5-trixie@sha256:8e8f9c84609b6005af0a4a8227cee53d6226aab1c6dcb22daf5aeeb8b05480e1 AS builder
ARG TARGETOS
ARG TARGETARCH
@@ -47,7 +47,7 @@ RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \
go build -o /go/proxy ./cmd/proxy
# Stage 3: Run
FROM ghcr.io/goauthentik/fips-debian:trixie-slim-fips@sha256:2f19fc114923ec0842329bf638cb155e597c4be9c8119a3db038ffc3fede9228
FROM ghcr.io/goauthentik/fips-debian:trixie-slim-fips@sha256:10dadf1df1337e8eb4218acef6a3027abebf0a155a95d792af736f85ab0e9588
ARG VERSION
ARG GIT_BUILD_HASH

View File

@@ -6,7 +6,6 @@ authors = [{ name = "authentik Team", email = "hello@goauthentik.io" }]
requires-python = "==3.13.*"
dependencies = [
"ak-guardian==3.2.0",
"akql",
"argon2-cffi==25.1.0",
"channels==4.3.1",
"cryptography==45.0.5",
@@ -27,6 +26,7 @@ dependencies = [
"django-prometheus==2.4.1",
"django-storages[s3]==1.14.6",
"django-tenants==3.9.0",
"djangoql==0.18.1",
"djangorestframework==3.16.1",
"docker==7.1.0",
"drf-orjson-renderer==1.7.3",
@@ -121,7 +121,6 @@ no-binary-package = [
[tool.uv.sources]
ak-guardian = { workspace = true }
akql = { workspace = true }
django-channels-postgres = { workspace = true }
django-dramatiq-postgres = { workspace = true }
django-postgres-cache = { workspace = true }
@@ -130,7 +129,6 @@ opencontainers = { git = "https://github.com/vsoch/oci-python", rev = "ceb4fcc09
[tool.uv.workspace]
members = [
"packages/ak-guardian",
"packages/akql",
"packages/django-channels-postgres",
"packages/django-dramatiq-postgres",
"packages/django-postgres-cache",

View File

@@ -1,7 +1,7 @@
# syntax=docker/dockerfile:1
# Stage 1: Build
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.25.5-trixie@sha256:5d35fb8d28b9095d123b7d96095bbf3750ff18be0a87e5a21c9cffc4351fbf96 AS builder
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.25.5-trixie@sha256:8e8f9c84609b6005af0a4a8227cee53d6226aab1c6dcb22daf5aeeb8b05480e1 AS builder
ARG TARGETOS
ARG TARGETARCH

View File

@@ -1,7 +1,7 @@
# syntax=docker/dockerfile:1
# Stage 1: Build
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.25.5-trixie@sha256:5d35fb8d28b9095d123b7d96095bbf3750ff18be0a87e5a21c9cffc4351fbf96 AS builder
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.25.5-trixie@sha256:8e8f9c84609b6005af0a4a8227cee53d6226aab1c6dcb22daf5aeeb8b05480e1 AS builder
ARG TARGETOS
ARG TARGETARCH
@@ -31,7 +31,7 @@ RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \
go build -o /go/radius ./cmd/radius
# Stage 2: Run
FROM ghcr.io/goauthentik/fips-debian:trixie-slim-fips@sha256:2f19fc114923ec0842329bf638cb155e597c4be9c8119a3db038ffc3fede9228
FROM ghcr.io/goauthentik/fips-debian:trixie-slim-fips@sha256:10dadf1df1337e8eb4218acef6a3027abebf0a155a95d792af736f85ab0e9588
ARG VERSION
ARG GIT_BUILD_HASH

View File

@@ -33581,7 +33581,8 @@ components:
minLength: 1
default: ak-stage-authenticator-email
code:
type: integer
type: string
minLength: 1
email:
type: string
minLength: 1
@@ -33828,7 +33829,8 @@ components:
minLength: 1
default: ak-stage-authenticator-sms
code:
type: integer
type: string
minLength: 1
phone_number:
type: string
minLength: 1
@@ -34102,7 +34104,8 @@ components:
minLength: 1
default: ak-stage-authenticator-totp
code:
type: integer
type: string
minLength: 1
required:
- code
AuthenticatorTOTPStage:

6
scripts/generate_docker_compose.py Normal file → Executable file
View File

@@ -1,3 +1,5 @@
#!/usr/bin/env python3
from yaml import safe_dump
from authentik import authentik_version
@@ -42,7 +44,7 @@ base = {
"image": authentik_image,
"ports": ["${COMPOSE_PORT_HTTP:-9000}:9000", "${COMPOSE_PORT_HTTPS:-9443}:9443"],
"restart": "unless-stopped",
"volumes": ["./media:/data/media", "./custom-templates:/templates"],
"volumes": ["./data:/data", "./custom-templates:/templates"],
},
"worker": {
"command": "worker",
@@ -62,7 +64,7 @@ base = {
"user": "root",
"volumes": [
"/var/run/docker.sock:/var/run/docker.sock",
"./media:/data/media",
"./data:/data",
"./certs:/certs",
"./custom-templates:/templates",
],

View File

@@ -249,36 +249,60 @@ class SeleniumTestCase(DockerTestCase, StaticLiveServerTestCase):
Raises a clear test failure if the element isn't found, the text doesn't appear
within `timeout` seconds, or the text is not valid JSON.
"""
use_body = context is None
wait_timeout = timeout or self.wait_timeout
def get_context() -> WebElement:
"""Get or refresh the context element."""
if use_body:
return self.driver.find_element(By.TAG_NAME, "body")
return context
def get_text_safely() -> str:
"""Get element text, re-finding element if stale."""
for _ in range(5):
try:
return get_context().text.strip()
except StaleElementReferenceException:
sleep(0.5)
return get_context().text.strip()
def get_inner_html_safely() -> str:
"""Get innerHTML, re-finding element if stale."""
for _ in range(5):
try:
return get_context().get_attribute("innerHTML") or ""
except StaleElementReferenceException:
sleep(0.5)
return get_context().get_attribute("innerHTML") or ""
try:
if context is None:
context = self.driver.find_element(By.TAG_NAME, "body")
get_context()
except NoSuchElementException:
self.fail(
f"No element found (defaulted to <body>). Current URL: {self.driver.current_url}"
)
wait_timeout = timeout or self.wait_timeout
wait = WebDriverWait(context, wait_timeout)
wait = WebDriverWait(self.driver, wait_timeout)
try:
wait.until(lambda d: len(d.text.strip()) != 0)
wait.until(lambda d: len(get_text_safely()) != 0)
except TimeoutException:
snippet = context.text.strip()[:500].replace("\n", " ")
snippet = get_text_safely()[:500].replace("\n", " ")
self.fail(
f"Timed out waiting for element text to appear at {self.driver.current_url}. "
f"Current content: {snippet or '<empty>'}"
)
body_text = context.text.strip()
inner_html = context.get_attribute("innerHTML") or ""
body_text = get_text_safely()
inner_html = get_inner_html_safely()
if "redirecting" in inner_html.lower():
try:
wait.until(lambda d: "redirecting" not in d.get_attribute("innerHTML").lower())
wait.until(lambda d: "redirecting" not in get_inner_html_safely().lower())
except TimeoutException:
snippet = context.text.strip()[:500].replace("\n", " ")
inner_html = context.get_attribute("innerHTML") or ""
snippet = get_text_safely()[:500].replace("\n", " ")
inner_html = get_inner_html_safely()
self.fail(
f"Timed out waiting for redirect to finish at {self.driver.current_url}. "
@@ -286,8 +310,8 @@ class SeleniumTestCase(DockerTestCase, StaticLiveServerTestCase):
f"{inner_html or '<empty>'}"
)
inner_html = context.get_attribute("innerHTML") or ""
body_text = context.text.strip()
inner_html = get_inner_html_safely()
body_text = get_text_safely()
snippet = body_text[:500].replace("\n", " ")

27
uv.lock generated
View File

@@ -5,7 +5,6 @@ requires-python = "==3.13.*"
[manifest]
members = [
"ak-guardian",
"akql",
"authentik",
"django-channels-postgres",
"django-dramatiq-postgres",
@@ -94,17 +93,6 @@ requires-dist = [
{ name = "typing-extensions", marker = "python_full_version < '3.15'", specifier = ">=4.12.0" },
]
[[package]]
name = "akql"
version = "3.2.0"
source = { editable = "packages/akql" }
dependencies = [
{ name = "ply" },
]
[package.metadata]
requires-dist = [{ name = "ply", specifier = ">=3.8" }]
[[package]]
name = "annotated-types"
version = "0.7.0"
@@ -201,7 +189,6 @@ version = "2026.2.0rc1"
source = { editable = "." }
dependencies = [
{ name = "ak-guardian" },
{ name = "akql" },
{ name = "argon2-cffi" },
{ name = "channels" },
{ name = "cryptography" },
@@ -222,6 +209,7 @@ dependencies = [
{ name = "django-prometheus" },
{ name = "django-storages", extra = ["s3"] },
{ name = "django-tenants" },
{ name = "djangoql" },
{ name = "djangorestframework" },
{ name = "docker" },
{ name = "drf-orjson-renderer" },
@@ -305,7 +293,6 @@ dev = [
[package.metadata]
requires-dist = [
{ name = "ak-guardian", editable = "packages/ak-guardian" },
{ name = "akql", editable = "packages/akql" },
{ name = "argon2-cffi", specifier = "==25.1.0" },
{ name = "channels", specifier = "==4.3.1" },
{ name = "cryptography", specifier = "==45.0.5" },
@@ -326,6 +313,7 @@ requires-dist = [
{ name = "django-prometheus", specifier = "==2.4.1" },
{ name = "django-storages", extras = ["s3"], specifier = "==1.14.6" },
{ name = "django-tenants", specifier = "==3.9.0" },
{ name = "djangoql", specifier = "==0.18.1" },
{ name = "djangorestframework", specifier = "==3.16.1" },
{ name = "docker", specifier = "==7.1.0" },
{ name = "drf-orjson-renderer", specifier = "==1.7.3" },
@@ -1264,6 +1252,17 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/57/918cfca627fcdc3441981dddc72a22be02e57abdb5391eb7339ea77a5ef4/django_tenants-3.9.0-py3-none-any.whl", hash = "sha256:14421088a4336444e2c4af54f21a6af2e57e53dcf95ba5d19b5fa17142cb460b", size = 215955, upload-time = "2025-09-06T21:46:05.939Z" },
]
[[package]]
name = "djangoql"
version = "0.18.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "ply" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/9e/0a/83cdb7b9d3b854b98941363153945f6c051b3bc50cd61108a85677c98c3a/djangoql-0.18.1-py2.py3-none-any.whl", hash = "sha256:51b3085a805627ebb43cfd0aa861137cdf8f69cc3c9244699718fe04a6c8e26d", size = 218209, upload-time = "2024-01-08T14:10:47.915Z" },
]
[[package]]
name = "djangorestframework"
version = "3.16.1"

1447
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -93,15 +93,15 @@
"@codemirror/lang-xml": "^6.1.0",
"@codemirror/legacy-modes": "^6.5.2",
"@codemirror/theme-one-dark": "^6.1.3",
"@eslint/js": "^9.39.1",
"@eslint/js": "^9.39.2",
"@floating-ui/dom": "^1.7.4",
"@formatjs/intl-listformat": "^7.7.13",
"@fortawesome/fontawesome-free": "^7.1.0",
"@goauthentik/api": "^2025.10.0-rc1-1760614339",
"@goauthentik/core": "^1.0.0",
"@goauthentik/esbuild-plugin-live-reload": "^1.3.1",
"@goauthentik/eslint-config": "^1.1.1",
"@goauthentik/prettier-config": "^3.2.1",
"@goauthentik/esbuild-plugin-live-reload": "^1.4.0",
"@goauthentik/eslint-config": "^1.2.0",
"@goauthentik/prettier-config": "^3.3.1",
"@goauthentik/tsconfig": "^1.0.5",
"@hcaptcha/types": "^1.1.0",
"@lit/context": "^1.1.6",
@@ -117,20 +117,20 @@
"@patternfly/elements": "^4.2.0",
"@patternfly/patternfly": "^4.224.2",
"@playwright/test": "^1.57.0",
"@sentry/browser": "^10.29.0",
"@storybook/addon-docs": "^10.1.7",
"@storybook/addon-links": "^10.1.7",
"@storybook/web-components": "^10.1.7",
"@storybook/web-components-vite": "^10.1.7",
"@sentry/browser": "^10.31.0",
"@storybook/addon-docs": "^10.1.10",
"@storybook/addon-links": "^10.1.10",
"@storybook/web-components": "^10.1.10",
"@storybook/web-components-vite": "^10.1.10",
"@types/codemirror": "^5.60.17",
"@types/grecaptcha": "^3.0.9",
"@types/guacamole-common-js": "^1.5.5",
"@types/node": "^25.0.0",
"@types/node": "^25.0.3",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"@typescript-eslint/eslint-plugin": "^8.47.0",
"@typescript-eslint/parser": "^8.47.0",
"@vitest/browser": "^4.0.15",
"@typescript-eslint/eslint-plugin": "^8.50.0",
"@typescript-eslint/parser": "^8.50.0",
"@vitest/browser": "^4.0.16",
"@vitest/browser-playwright": "^4.0.15",
"@webcomponents/webcomponentsjs": "^2.8.0",
"base64-js": "^1.5.1",
@@ -143,15 +143,15 @@
"date-fns": "^4.1.0",
"deepmerge-ts": "^7.1.5",
"dompurify": "^3.3.1",
"esbuild": "^0.27.1",
"eslint": "^9.39.1",
"esbuild": "^0.27.2",
"eslint": "^9.39.2",
"eslint-plugin-lit": "^2.1.1",
"eslint-plugin-wc": "^3.0.2",
"fuse.js": "^7.1.0",
"globals": "^16.5.0",
"guacamole-common-js": "^1.5.0",
"hastscript": "^9.0.1",
"knip": "^5.73.1",
"knip": "^5.75.1",
"lex": "^2025.11.0",
"lit": "^3.3.1",
"lit-analyzer": "^2.0.3",
@@ -182,9 +182,9 @@
"turnstile-types": "^1.2.3",
"type-fest": "^5.3.1",
"typescript": "^5.9.3",
"typescript-eslint": "^8.49.0",
"typescript-eslint": "^8.50.0",
"unist-util-visit": "^5.0.0",
"vite": "^7.2.7",
"vite": "^7.3.0",
"vitest": "^4.0.15",
"webcomponent-qr-code": "^1.3.0",
"wireit": "^0.14.12",
@@ -194,10 +194,10 @@
"@esbuild/darwin-arm64": "^0.27.0",
"@esbuild/linux-arm64": "^0.27.0",
"@esbuild/linux-x64": "^0.27.0",
"@rollup/rollup-darwin-arm64": "^4.53.3",
"@rollup/rollup-linux-arm64-gnu": "^4.53.3",
"@rollup/rollup-linux-x64-gnu": "^4.53.3",
"chromedriver": "^143.0.1"
"@rollup/rollup-darwin-arm64": "^4.53.5",
"@rollup/rollup-linux-arm64-gnu": "^4.53.5",
"@rollup/rollup-linux-x64-gnu": "^4.53.5",
"chromedriver": "^143.0.2"
},
"wireit": {
"build": {

View File

@@ -44,7 +44,7 @@
},
"dependencies": {
"@goauthentik/tsconfig": "^1.0.5",
"@types/node": "^25.0.0",
"@types/node": "^25.0.3",
"@types/semver": "^7.7.1",
"semver": "^7.7.3",
"typescript": "^5.9.3"

View File

@@ -20,13 +20,13 @@
"@rollup/plugin-node-resolve": "^16.0.3",
"@rollup/plugin-swc": "^0.4.0",
"@swc/cli": "^0.7.9",
"@swc/core": "^1.15.3",
"@swc/core": "^1.15.6",
"base64-js": "^1.5.1",
"bootstrap": "^5.3.8",
"formdata-polyfill": "^2025.11.0",
"globby": "16.0.0",
"jquery": "^3.7.1",
"rollup": "^4.53.3",
"rollup": "^4.53.5",
"weakmap-polyfill": "^2.0.4"
},
"optionalDependencies": {

View File

@@ -77,7 +77,11 @@ export class AgentConnectorSetup extends AKElement {
<p>${msg("Afterwards, select the enrollment token you want to use:")}</p>
</div>
<div class="pf-l-grid__item pf-m-12-col">
<p>${msg("Then download the configuration to deploy the authentik Agent")}</p>
<p>
${msg(
"Next, download the configuration to deploy the authentik Agent via MDM",
)}
</p>
</div>
</div>
<div class="pf-l-grid__item pf-m-6-col pf-l-grid">

View File

@@ -77,10 +77,13 @@ export class EnrollmentTokenForm extends WithBrandConfig(ModelForm<EnrollmentTok
value=${ifDefined(this.instance?.name)}
required
></ak-text-input>
<ak-form-element-horizontal label=${msg("Device Group")} name="deviceGroup">
<ak-form-element-horizontal label=${msg("Device Access Group")} name="deviceGroup">
<ak-endpoints-device-group-search
.group=${this.instance?.deviceGroup}
></ak-endpoints-device-group-search>
<p class="pf-c-form__helper-text">
${msg("Select a device access group to be added to upon enrollment.")}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal name="expiring">
<label class="pf-c-switch">

View File

@@ -68,7 +68,7 @@ export class DeviceViewPage extends AKElement {
? msg(str`Device ${this.device?.name}`)
: msg("Loading device..."),
description: this.device?.facts.data.os
? this.device?.facts.data.os?.name + " " + this.device?.facts.data.os?.version
? `${this.device?.facts.data.os?.name} ${this.device?.facts.data.os?.version}`
: undefined,
icon: "fa fa-laptop",
});
@@ -110,7 +110,7 @@ export class DeviceViewPage extends AKElement {
?good=${this.device.facts.data.network?.firewallEnabled}
></ak-status-label>`,
],
[msg("Group"), this.device.accessGroupObj?.name ?? "-"],
[msg("Device access group"), this.device.accessGroupObj?.name ?? "-"],
[
msg("Actions"),
html`<ak-forms-modal>
@@ -162,13 +162,13 @@ export class DeviceViewPage extends AKElement {
></ak-status-label>`,
],
[
msg("Disk size"),
msg("Primary disk size"),
rootDisk?.capacityTotalBytes
? getSize(rootDisk.capacityTotalBytes)
: "-",
],
[
msg("Disk usage"),
msg("Primary disk usage"),
rootDisk?.capacityTotalBytes && rootDisk.capacityUsedBytes
? html`<progress
value="${rootDisk.capacityUsedBytes}"

View File

@@ -91,6 +91,20 @@ export class DataExportListPage extends TablePage<DataExport> {
</div>
</dl>`;
}
protected renderEmpty(_inner?: TemplateResult): TemplateResult {
return super.renderEmpty(
html`<ak-empty-state icon=${this.pageIcon}
><span
>${msg(
html`To create a data export, navigate to
<a href="#/identity/users">Directory > Users</a> or to
<a href="#/events/log">Events > Logs</a>.`,
)}</span
>
</ak-empty-state>`,
);
}
}
declare global {

View File

@@ -16,18 +16,33 @@ import { createRef, ref } from "lit/directives/ref.js";
// Same regex is used in the backend as well
const VALID_FILE_NAME_PATTERN = /^[a-zA-Z0-9._/-]+$/;
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/source
// This is perfect for the "pattern" attribute
const VALID_FILE_NAME_PATTERN_STRING = VALID_FILE_NAME_PATTERN.source;
// Note: browsers compile `pattern` using the new `v` RegExp flag (Unicode sets). Under `/v`,
// both `/` and `-` must be escaped inside character classes.
const VALID_FILE_NAME_PATTERN_STRING = "^[a-zA-Z0-9._\\/\\-]+$";
function assertValidFileName(fileName: string): void {
if (!VALID_FILE_NAME_PATTERN.test(fileName)) {
throw new Error(
msg("Filename can only contain letters, numbers, dots, hyphens, and underscores"),
msg(
"Filename can only contain letters, numbers, dots, hyphens, underscores, and slashes",
),
);
}
}
function getFileExtension(fileName: string): string {
const lastDot = fileName.lastIndexOf(".");
if (lastDot <= 0) return "";
return fileName.slice(lastDot);
}
function hasBasenameExtension(fileName: string): boolean {
const baseName = fileName.split("/").pop() ?? fileName;
const lastDot = baseName.lastIndexOf(".");
return lastDot > 0;
}
@customElement("ak-file-upload-form")
export class FileUploadForm extends Form<Record<string, unknown>> {
@property({ type: String, useDefault: true })
@@ -57,36 +72,36 @@ export class FileUploadForm extends Form<Record<string, unknown>> {
throw new PreventFormSubmit("Selected file not provided", this);
}
assertValidFileName(this.selectedFile.name);
const api = new AdminApi(DEFAULT_CONFIG);
const customName = typeof data.fileName === "string" ? data.fileName.trim() : "";
const customName = typeof data.name === "string" ? data.name.trim() : "";
// If custom name provided, validate and append original extension
// Only validate the original filename if no custom name is provided
let finalName = this.selectedFile.name;
if (customName) {
assertValidFileName(customName);
const ext = this.selectedFile.name.substring(this.selectedFile.name.lastIndexOf("."));
finalName = customName + ext;
const ext = getFileExtension(this.selectedFile.name);
finalName =
ext && !hasBasenameExtension(customName) ? `${customName}${ext}` : customName;
} else {
assertValidFileName(this.selectedFile.name);
}
return api
.adminFileCreate({
file: this.selectedFile,
name: finalName,
usage: this.usage,
})
.then(() => {
showMessage({
level: MessageLevel.success,
message: msg("File uploaded successfully"),
});
assertValidFileName(finalName);
this.reset();
})
.finally(() => {
this.clearFileInput();
});
await api.adminFileCreate({
file: this.selectedFile,
name: finalName,
usage: this.usage,
});
showMessage({
level: MessageLevel.success,
message: msg("File uploaded successfully"),
});
this.reset();
this.clearFileInput();
}
renderForm() {
@@ -101,7 +116,7 @@ export class FileUploadForm extends Form<Record<string, unknown>> {
@change=${this.#fileChangeListener}
/>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("File Name")} name="fileName">
<ak-form-element-horizontal label=${msg("File Name")} name="name">
<input
type="text"
class="pf-c-form-control"

View File

@@ -71,40 +71,45 @@ export class RoleObjectPermissionForm extends ModelForm<RoleAssignData, number>
if (!this.modelPermissions) {
return nothing;
}
return html`<form class="pf-c-form pf-m-horizontal">
<ak-form-element-horizontal label=${msg("Role")} name="role">
<ak-search-select
.fetchObjects=${async (query?: string): Promise<Role[]> => {
const args: RbacRolesListRequest = {
ordering: "name",
};
if (query !== undefined) {
args.search = query;
}
const roles = await new RbacApi(DEFAULT_CONFIG).rbacRolesList(args);
return roles.results;
}}
.renderElement=${(role: Role): string => {
return role.name;
}}
.value=${(role: Role | undefined): string | undefined => {
return role?.pk;
}}
>
</ak-search-select>
</ak-form-element-horizontal>
${this.modelPermissions?.results
.filter((perm) => {
const [_app, model] = this.model?.split(".") || "";
return perm.codename !== `add_${model}`;
})
.map((perm) => {
return html`<ak-switch-input
name="permissions.${perm.codename}"
label=${perm.name}
></ak-switch-input>`;
})}
</form>`;
return html`<span
>${msg(
"Choose the object permissions that you want the selected role to have on this object. These object permissions are in addition to any global permissions already within the role.",
)}</span
>
<form class="pf-c-form pf-m-horizontal">
<ak-form-element-horizontal label=${msg("Role")} name="role">
<ak-search-select
.fetchObjects=${async (query?: string): Promise<Role[]> => {
const args: RbacRolesListRequest = {
ordering: "name",
};
if (query !== undefined) {
args.search = query;
}
const roles = await new RbacApi(DEFAULT_CONFIG).rbacRolesList(args);
return roles.results;
}}
.renderElement=${(role: Role): string => {
return role.name;
}}
.value=${(role: Role | undefined): string | undefined => {
return role?.pk;
}}
>
</ak-search-select>
</ak-form-element-horizontal>
${this.modelPermissions?.results
.filter((perm) => {
const [_app, model] = this.model?.split(".") || "";
return perm.codename !== `add_${model}`;
})
.map((perm) => {
return html`<ak-switch-input
name="permissions.${perm.codename}"
label=${perm.name}
></ak-switch-input>`;
})}
</form>`;
}
}

View File

@@ -87,7 +87,7 @@ export class RoleAssignedObjectPermissionTable extends Table<RoleAssignedObjectP
renderObjectCreate(): TemplateResult {
return html`<ak-forms-modal>
<span slot="submit">${msg("Assign")}</span>
<span slot="header">${msg("Assign permission to role")}</span>
<span slot="header">${msg("Assign object permissions to role")}</span>
<ak-rbac-role-object-permission-form
model=${ifDefined(this.model)}
objectPk=${ifDefined(this.objectPk)}
@@ -95,7 +95,7 @@ export class RoleAssignedObjectPermissionTable extends Table<RoleAssignedObjectP
>
</ak-rbac-role-object-permission-form>
<button slot="trigger" class="pf-c-button pf-m-primary">
${msg("Assign role permissions")}
${msg("Assign Object Permission")}
</button>
</ak-forms-modal>`;
}
@@ -135,9 +135,9 @@ export class RoleAssignedObjectPermissionTable extends Table<RoleAssignedObjectP
const assignedToModel = item.modelPermissions.some(
(uperm) => uperm.codename === perm.codename,
);
const assignedToObject = item.objectPermissions.some(
(uperm) => uperm.codename === perm.codename,
);
const assignedToObject = item.objectPermissions
.filter((uperm) => uperm.objectPk === this.objectPk)
.some((uperm) => uperm.codename === perm.codename);
let tooltip: string | null = null;
if (assignedToModel && assignedToObject) {

View File

@@ -51,6 +51,11 @@ export class NavigationButtons extends WithSession(AKElement) {
Styles,
];
connectedCallback(): void {
super.connectedCallback();
this.refreshNotifications();
}
protected async refreshNotifications(): Promise<void> {
const { currentUser } = this;

View File

@@ -4183,9 +4183,6 @@ neprojde, když jedna nebo obě z vybraných možností jsou rovny nebo nad prah
<trans-unit id="sf96a86df0756bc7b">
<source>Afterwards, select the enrollment token you want to use:</source>
</trans-unit>
<trans-unit id="s13b4cf044a01dde2">
<source>Then download the configuration to deploy the authentik Agent</source>
</trans-unit>
<trans-unit id="s12f523a52b843ea2">
<source>macOS</source>
</trans-unit>
@@ -4284,12 +4281,6 @@ neprojde, když jedna nebo obě z vybraných možností jsou rovny nebo nad prah
<trans-unit id="s94a50f1495fb5ccd">
<source>Disk encryption</source>
</trans-unit>
<trans-unit id="s76ea179414f2b2a5">
<source>Disk size</source>
</trans-unit>
<trans-unit id="s25e7a078391a3ec3">
<source>Disk usage</source>
</trans-unit>
<trans-unit id="s416211a967a6db4e">
<source>Users / Groups</source>
</trans-unit>
@@ -9550,9 +9541,6 @@ Vazby na skupiny/uživatele jsou kontrolovány vůči uživateli události.</tar
<trans-unit id="s6df8326edea3b23d">
<source>If no device was provided, this stage will stop flow execution.</source>
</trans-unit>
<trans-unit id="s506c7d2e87f6770e">
<source>Filename can only contain letters, numbers, dots, hyphens, and underscores</source>
</trans-unit>
<trans-unit id="s3a3d5b2575cd32ea">
<source>File uploaded successfully</source>
</trans-unit>
@@ -9685,9 +9673,6 @@ Vazby na skupiny/uživatele jsou kontrolovány vůči uživateli události.</tar
<trans-unit id="sc47f8ab6162bb2bb">
<source>Outpost configuration</source>
</trans-unit>
<trans-unit id="s184c3b30bebb2dd8">
<source>Assign role permissions</source>
</trans-unit>
<trans-unit id="s11a2ac9f2bd811d8">
<source>Delete Object Permission</source>
</trans-unit>
@@ -9901,6 +9886,41 @@ Vazby na skupiny/uživatele jsou kontrolovány vůči uživateli události.</tar
<trans-unit id="s46a03121a2c260ea">
<source>Buffer PolicyAccessView requests</source>
</trans-unit>
<trans-unit id="s361e9d929ee925e6">
<source>Assign Object Permission</source>
</trans-unit>
<trans-unit id="s5f6ad947b4824e40">
<source>Next, download the configuration to deploy the authentik Agent via MDM</source>
</trans-unit>
<trans-unit id="s523337424b694d5c">
<source>Device Access Group</source>
</trans-unit>
<trans-unit id="s7642bf28cf8f476c">
<source>Select a device access group to be added to upon enrollment.</source>
</trans-unit>
<trans-unit id="h30bbf18fbf87fa57">
<source>To create a data export, navigate to
<x id="0" equiv-text="&lt;a href=&quot;#/identity/users&quot;&gt;"/>Directory &gt; Users<x id="1" equiv-text="&lt;/a&gt;"/> or to
<x id="2" equiv-text="&lt;a href=&quot;#/events/log&quot;&gt;"/>Events &gt; Logs<x id="3" equiv-text="&lt;/a&gt;"/>.</source>
</trans-unit>
<trans-unit id="s813faec5ff1d32d1">
<source>Filename can only contain letters, numbers, dots, hyphens, underscores, and slashes</source>
</trans-unit>
<trans-unit id="s54af3ec70642782c">
<source>Choose the object permissions that you want the selected role to have on this object. These object permissions are in addition to any global permissions already within the role.</source>
</trans-unit>
<trans-unit id="sb3be65525dd1f92c">
<source>Assign object permissions to role</source>
</trans-unit>
<trans-unit id="sbcd108b66363075c">
<source>Device access group</source>
</trans-unit>
<trans-unit id="s325acae04cdcac57">
<source>Primary disk size</source>
</trans-unit>
<trans-unit id="sb4a957846c89fca1">
<source>Primary disk usage</source>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -4214,9 +4214,6 @@ doesn't pass when either or both of the selected options are equal or above the
<trans-unit id="sf96a86df0756bc7b">
<source>Afterwards, select the enrollment token you want to use:</source>
</trans-unit>
<trans-unit id="s13b4cf044a01dde2">
<source>Then download the configuration to deploy the authentik Agent</source>
</trans-unit>
<trans-unit id="s12f523a52b843ea2">
<source>macOS</source>
</trans-unit>
@@ -4315,12 +4312,6 @@ doesn't pass when either or both of the selected options are equal or above the
<trans-unit id="s94a50f1495fb5ccd">
<source>Disk encryption</source>
</trans-unit>
<trans-unit id="s76ea179414f2b2a5">
<source>Disk size</source>
</trans-unit>
<trans-unit id="s25e7a078391a3ec3">
<source>Disk usage</source>
</trans-unit>
<trans-unit id="s416211a967a6db4e">
<source>Users / Groups</source>
</trans-unit>
@@ -9590,9 +9581,6 @@ Bindings zu Gruppen/Benutzern werden mit dem Benutzer des Ereignisses abgegliche
<trans-unit id="s6df8326edea3b23d">
<source>If no device was provided, this stage will stop flow execution.</source>
</trans-unit>
<trans-unit id="s506c7d2e87f6770e">
<source>Filename can only contain letters, numbers, dots, hyphens, and underscores</source>
</trans-unit>
<trans-unit id="s3a3d5b2575cd32ea">
<source>File uploaded successfully</source>
</trans-unit>
@@ -9725,9 +9713,6 @@ Bindings zu Gruppen/Benutzern werden mit dem Benutzer des Ereignisses abgegliche
<trans-unit id="sc47f8ab6162bb2bb">
<source>Outpost configuration</source>
</trans-unit>
<trans-unit id="s184c3b30bebb2dd8">
<source>Assign role permissions</source>
</trans-unit>
<trans-unit id="s11a2ac9f2bd811d8">
<source>Delete Object Permission</source>
</trans-unit>
@@ -9941,6 +9926,41 @@ Bindings zu Gruppen/Benutzern werden mit dem Benutzer des Ereignisses abgegliche
<trans-unit id="s46a03121a2c260ea">
<source>Buffer PolicyAccessView requests</source>
</trans-unit>
<trans-unit id="s361e9d929ee925e6">
<source>Assign Object Permission</source>
</trans-unit>
<trans-unit id="s5f6ad947b4824e40">
<source>Next, download the configuration to deploy the authentik Agent via MDM</source>
</trans-unit>
<trans-unit id="s523337424b694d5c">
<source>Device Access Group</source>
</trans-unit>
<trans-unit id="s7642bf28cf8f476c">
<source>Select a device access group to be added to upon enrollment.</source>
</trans-unit>
<trans-unit id="h30bbf18fbf87fa57">
<source>To create a data export, navigate to
<x id="0" equiv-text="&lt;a href=&quot;#/identity/users&quot;&gt;"/>Directory &gt; Users<x id="1" equiv-text="&lt;/a&gt;"/> or to
<x id="2" equiv-text="&lt;a href=&quot;#/events/log&quot;&gt;"/>Events &gt; Logs<x id="3" equiv-text="&lt;/a&gt;"/>.</source>
</trans-unit>
<trans-unit id="s813faec5ff1d32d1">
<source>Filename can only contain letters, numbers, dots, hyphens, underscores, and slashes</source>
</trans-unit>
<trans-unit id="s54af3ec70642782c">
<source>Choose the object permissions that you want the selected role to have on this object. These object permissions are in addition to any global permissions already within the role.</source>
</trans-unit>
<trans-unit id="sb3be65525dd1f92c">
<source>Assign object permissions to role</source>
</trans-unit>
<trans-unit id="sbcd108b66363075c">
<source>Device access group</source>
</trans-unit>
<trans-unit id="s325acae04cdcac57">
<source>Primary disk size</source>
</trans-unit>
<trans-unit id="sb4a957846c89fca1">
<source>Primary disk usage</source>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -3220,9 +3220,6 @@ doesn't pass when either or both of the selected options are equal or above the
<trans-unit id="sf96a86df0756bc7b">
<source>Afterwards, select the enrollment token you want to use:</source>
</trans-unit>
<trans-unit id="s13b4cf044a01dde2">
<source>Then download the configuration to deploy the authentik Agent</source>
</trans-unit>
<trans-unit id="s12f523a52b843ea2">
<source>macOS</source>
</trans-unit>
@@ -3319,12 +3316,6 @@ doesn't pass when either or both of the selected options are equal or above the
<trans-unit id="s94a50f1495fb5ccd">
<source>Disk encryption</source>
</trans-unit>
<trans-unit id="s76ea179414f2b2a5">
<source>Disk size</source>
</trans-unit>
<trans-unit id="s25e7a078391a3ec3">
<source>Disk usage</source>
</trans-unit>
<trans-unit id="s416211a967a6db4e">
<source>Users / Groups</source>
</trans-unit>
@@ -7383,9 +7374,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="s6df8326edea3b23d">
<source>If no device was provided, this stage will stop flow execution.</source>
</trans-unit>
<trans-unit id="s506c7d2e87f6770e">
<source>Filename can only contain letters, numbers, dots, hyphens, and underscores</source>
</trans-unit>
<trans-unit id="s3a3d5b2575cd32ea">
<source>File uploaded successfully</source>
</trans-unit>
@@ -7518,9 +7506,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="sc47f8ab6162bb2bb">
<source>Outpost configuration</source>
</trans-unit>
<trans-unit id="s184c3b30bebb2dd8">
<source>Assign role permissions</source>
</trans-unit>
<trans-unit id="s11a2ac9f2bd811d8">
<source>Delete Object Permission</source>
</trans-unit>
@@ -7734,6 +7719,41 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="s46a03121a2c260ea">
<source>Buffer PolicyAccessView requests</source>
</trans-unit>
<trans-unit id="s361e9d929ee925e6">
<source>Assign Object Permission</source>
</trans-unit>
<trans-unit id="s5f6ad947b4824e40">
<source>Next, download the configuration to deploy the authentik Agent via MDM</source>
</trans-unit>
<trans-unit id="s523337424b694d5c">
<source>Device Access Group</source>
</trans-unit>
<trans-unit id="s7642bf28cf8f476c">
<source>Select a device access group to be added to upon enrollment.</source>
</trans-unit>
<trans-unit id="h30bbf18fbf87fa57">
<source>To create a data export, navigate to
<x id="0" equiv-text="&lt;a href=&quot;#/identity/users&quot;&gt;"/>Directory &gt; Users<x id="1" equiv-text="&lt;/a&gt;"/> or to
<x id="2" equiv-text="&lt;a href=&quot;#/events/log&quot;&gt;"/>Events &gt; Logs<x id="3" equiv-text="&lt;/a&gt;"/>.</source>
</trans-unit>
<trans-unit id="s813faec5ff1d32d1">
<source>Filename can only contain letters, numbers, dots, hyphens, underscores, and slashes</source>
</trans-unit>
<trans-unit id="s54af3ec70642782c">
<source>Choose the object permissions that you want the selected role to have on this object. These object permissions are in addition to any global permissions already within the role.</source>
</trans-unit>
<trans-unit id="sb3be65525dd1f92c">
<source>Assign object permissions to role</source>
</trans-unit>
<trans-unit id="sbcd108b66363075c">
<source>Device access group</source>
</trans-unit>
<trans-unit id="s325acae04cdcac57">
<source>Primary disk size</source>
</trans-unit>
<trans-unit id="sb4a957846c89fca1">
<source>Primary disk usage</source>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -4151,9 +4151,6 @@ no se aprueba cuando una o ambas de las opciones seleccionadas son iguales o sup
<trans-unit id="sf96a86df0756bc7b">
<source>Afterwards, select the enrollment token you want to use:</source>
</trans-unit>
<trans-unit id="s13b4cf044a01dde2">
<source>Then download the configuration to deploy the authentik Agent</source>
</trans-unit>
<trans-unit id="s12f523a52b843ea2">
<source>macOS</source>
</trans-unit>
@@ -4252,12 +4249,6 @@ no se aprueba cuando una o ambas de las opciones seleccionadas son iguales o sup
<trans-unit id="s94a50f1495fb5ccd">
<source>Disk encryption</source>
</trans-unit>
<trans-unit id="s76ea179414f2b2a5">
<source>Disk size</source>
</trans-unit>
<trans-unit id="s25e7a078391a3ec3">
<source>Disk usage</source>
</trans-unit>
<trans-unit id="s416211a967a6db4e">
<source>Users / Groups</source>
</trans-unit>
@@ -9510,9 +9501,6 @@ Las vinculaciones a grupos/usuarios se verifican en función del usuario del eve
<trans-unit id="s6df8326edea3b23d">
<source>If no device was provided, this stage will stop flow execution.</source>
</trans-unit>
<trans-unit id="s506c7d2e87f6770e">
<source>Filename can only contain letters, numbers, dots, hyphens, and underscores</source>
</trans-unit>
<trans-unit id="s3a3d5b2575cd32ea">
<source>File uploaded successfully</source>
</trans-unit>
@@ -9645,9 +9633,6 @@ Las vinculaciones a grupos/usuarios se verifican en función del usuario del eve
<trans-unit id="sc47f8ab6162bb2bb">
<source>Outpost configuration</source>
</trans-unit>
<trans-unit id="s184c3b30bebb2dd8">
<source>Assign role permissions</source>
</trans-unit>
<trans-unit id="s11a2ac9f2bd811d8">
<source>Delete Object Permission</source>
</trans-unit>
@@ -9861,6 +9846,41 @@ Las vinculaciones a grupos/usuarios se verifican en función del usuario del eve
<trans-unit id="s46a03121a2c260ea">
<source>Buffer PolicyAccessView requests</source>
</trans-unit>
<trans-unit id="s361e9d929ee925e6">
<source>Assign Object Permission</source>
</trans-unit>
<trans-unit id="s5f6ad947b4824e40">
<source>Next, download the configuration to deploy the authentik Agent via MDM</source>
</trans-unit>
<trans-unit id="s523337424b694d5c">
<source>Device Access Group</source>
</trans-unit>
<trans-unit id="s7642bf28cf8f476c">
<source>Select a device access group to be added to upon enrollment.</source>
</trans-unit>
<trans-unit id="h30bbf18fbf87fa57">
<source>To create a data export, navigate to
<x id="0" equiv-text="&lt;a href=&quot;#/identity/users&quot;&gt;"/>Directory &gt; Users<x id="1" equiv-text="&lt;/a&gt;"/> or to
<x id="2" equiv-text="&lt;a href=&quot;#/events/log&quot;&gt;"/>Events &gt; Logs<x id="3" equiv-text="&lt;/a&gt;"/>.</source>
</trans-unit>
<trans-unit id="s813faec5ff1d32d1">
<source>Filename can only contain letters, numbers, dots, hyphens, underscores, and slashes</source>
</trans-unit>
<trans-unit id="s54af3ec70642782c">
<source>Choose the object permissions that you want the selected role to have on this object. These object permissions are in addition to any global permissions already within the role.</source>
</trans-unit>
<trans-unit id="sb3be65525dd1f92c">
<source>Assign object permissions to role</source>
</trans-unit>
<trans-unit id="sbcd108b66363075c">
<source>Device access group</source>
</trans-unit>
<trans-unit id="s325acae04cdcac57">
<source>Primary disk size</source>
</trans-unit>
<trans-unit id="sb4a957846c89fca1">
<source>Primary disk usage</source>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -4293,9 +4293,6 @@ läpäisy estyy kun jompi kumpi tai molemmat vaihtoehdot ylittävät raja-arvon.
<trans-unit id="sf96a86df0756bc7b">
<source>Afterwards, select the enrollment token you want to use:</source>
</trans-unit>
<trans-unit id="s13b4cf044a01dde2">
<source>Then download the configuration to deploy the authentik Agent</source>
</trans-unit>
<trans-unit id="s12f523a52b843ea2">
<source>macOS</source>
</trans-unit>
@@ -4394,12 +4391,6 @@ läpäisy estyy kun jompi kumpi tai molemmat vaihtoehdot ylittävät raja-arvon.
<trans-unit id="s94a50f1495fb5ccd">
<source>Disk encryption</source>
</trans-unit>
<trans-unit id="s76ea179414f2b2a5">
<source>Disk size</source>
</trans-unit>
<trans-unit id="s25e7a078391a3ec3">
<source>Disk usage</source>
</trans-unit>
<trans-unit id="s416211a967a6db4e">
<source>Users / Groups</source>
</trans-unit>
@@ -9773,9 +9764,6 @@ Liitokset käyttäjiin/ryhmiin tarkistetaan tapahtuman käyttäjästä.</target>
<trans-unit id="s6df8326edea3b23d">
<source>If no device was provided, this stage will stop flow execution.</source>
</trans-unit>
<trans-unit id="s506c7d2e87f6770e">
<source>Filename can only contain letters, numbers, dots, hyphens, and underscores</source>
</trans-unit>
<trans-unit id="s3a3d5b2575cd32ea">
<source>File uploaded successfully</source>
</trans-unit>
@@ -9908,9 +9896,6 @@ Liitokset käyttäjiin/ryhmiin tarkistetaan tapahtuman käyttäjästä.</target>
<trans-unit id="sc47f8ab6162bb2bb">
<source>Outpost configuration</source>
</trans-unit>
<trans-unit id="s184c3b30bebb2dd8">
<source>Assign role permissions</source>
</trans-unit>
<trans-unit id="s11a2ac9f2bd811d8">
<source>Delete Object Permission</source>
</trans-unit>
@@ -10124,6 +10109,41 @@ Liitokset käyttäjiin/ryhmiin tarkistetaan tapahtuman käyttäjästä.</target>
<trans-unit id="s46a03121a2c260ea">
<source>Buffer PolicyAccessView requests</source>
</trans-unit>
<trans-unit id="s361e9d929ee925e6">
<source>Assign Object Permission</source>
</trans-unit>
<trans-unit id="s5f6ad947b4824e40">
<source>Next, download the configuration to deploy the authentik Agent via MDM</source>
</trans-unit>
<trans-unit id="s523337424b694d5c">
<source>Device Access Group</source>
</trans-unit>
<trans-unit id="s7642bf28cf8f476c">
<source>Select a device access group to be added to upon enrollment.</source>
</trans-unit>
<trans-unit id="h30bbf18fbf87fa57">
<source>To create a data export, navigate to
<x id="0" equiv-text="&lt;a href=&quot;#/identity/users&quot;&gt;"/>Directory &gt; Users<x id="1" equiv-text="&lt;/a&gt;"/> or to
<x id="2" equiv-text="&lt;a href=&quot;#/events/log&quot;&gt;"/>Events &gt; Logs<x id="3" equiv-text="&lt;/a&gt;"/>.</source>
</trans-unit>
<trans-unit id="s813faec5ff1d32d1">
<source>Filename can only contain letters, numbers, dots, hyphens, underscores, and slashes</source>
</trans-unit>
<trans-unit id="s54af3ec70642782c">
<source>Choose the object permissions that you want the selected role to have on this object. These object permissions are in addition to any global permissions already within the role.</source>
</trans-unit>
<trans-unit id="sb3be65525dd1f92c">
<source>Assign object permissions to role</source>
</trans-unit>
<trans-unit id="sbcd108b66363075c">
<source>Device access group</source>
</trans-unit>
<trans-unit id="s325acae04cdcac57">
<source>Primary disk size</source>
</trans-unit>
<trans-unit id="sb4a957846c89fca1">
<source>Primary disk usage</source>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -4282,9 +4282,6 @@ doesn't pass when either or both of the selected options are equal or above the
<trans-unit id="sf96a86df0756bc7b">
<source>Afterwards, select the enrollment token you want to use:</source>
</trans-unit>
<trans-unit id="s13b4cf044a01dde2">
<source>Then download the configuration to deploy the authentik Agent</source>
</trans-unit>
<trans-unit id="s12f523a52b843ea2">
<source>macOS</source>
</trans-unit>
@@ -4383,12 +4380,6 @@ doesn't pass when either or both of the selected options are equal or above the
<trans-unit id="s94a50f1495fb5ccd">
<source>Disk encryption</source>
</trans-unit>
<trans-unit id="s76ea179414f2b2a5">
<source>Disk size</source>
</trans-unit>
<trans-unit id="s25e7a078391a3ec3">
<source>Disk usage</source>
</trans-unit>
<trans-unit id="s416211a967a6db4e">
<source>Users / Groups</source>
</trans-unit>
@@ -9758,9 +9749,6 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti
<trans-unit id="s6df8326edea3b23d">
<source>If no device was provided, this stage will stop flow execution.</source>
</trans-unit>
<trans-unit id="s506c7d2e87f6770e">
<source>Filename can only contain letters, numbers, dots, hyphens, and underscores</source>
</trans-unit>
<trans-unit id="s3a3d5b2575cd32ea">
<source>File uploaded successfully</source>
</trans-unit>
@@ -9893,9 +9881,6 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti
<trans-unit id="sc47f8ab6162bb2bb">
<source>Outpost configuration</source>
</trans-unit>
<trans-unit id="s184c3b30bebb2dd8">
<source>Assign role permissions</source>
</trans-unit>
<trans-unit id="s11a2ac9f2bd811d8">
<source>Delete Object Permission</source>
</trans-unit>
@@ -10109,6 +10094,41 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti
<trans-unit id="s46a03121a2c260ea">
<source>Buffer PolicyAccessView requests</source>
</trans-unit>
<trans-unit id="s361e9d929ee925e6">
<source>Assign Object Permission</source>
</trans-unit>
<trans-unit id="s5f6ad947b4824e40">
<source>Next, download the configuration to deploy the authentik Agent via MDM</source>
</trans-unit>
<trans-unit id="s523337424b694d5c">
<source>Device Access Group</source>
</trans-unit>
<trans-unit id="s7642bf28cf8f476c">
<source>Select a device access group to be added to upon enrollment.</source>
</trans-unit>
<trans-unit id="h30bbf18fbf87fa57">
<source>To create a data export, navigate to
<x id="0" equiv-text="&lt;a href=&quot;#/identity/users&quot;&gt;"/>Directory &gt; Users<x id="1" equiv-text="&lt;/a&gt;"/> or to
<x id="2" equiv-text="&lt;a href=&quot;#/events/log&quot;&gt;"/>Events &gt; Logs<x id="3" equiv-text="&lt;/a&gt;"/>.</source>
</trans-unit>
<trans-unit id="s813faec5ff1d32d1">
<source>Filename can only contain letters, numbers, dots, hyphens, underscores, and slashes</source>
</trans-unit>
<trans-unit id="s54af3ec70642782c">
<source>Choose the object permissions that you want the selected role to have on this object. These object permissions are in addition to any global permissions already within the role.</source>
</trans-unit>
<trans-unit id="sb3be65525dd1f92c">
<source>Assign object permissions to role</source>
</trans-unit>
<trans-unit id="sbcd108b66363075c">
<source>Device access group</source>
</trans-unit>
<trans-unit id="s325acae04cdcac57">
<source>Primary disk size</source>
</trans-unit>
<trans-unit id="sb4a957846c89fca1">
<source>Primary disk usage</source>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -4110,9 +4110,6 @@ doesn't pass when either or both of the selected options are equal or above the
<trans-unit id="sf96a86df0756bc7b">
<source>Afterwards, select the enrollment token you want to use:</source>
</trans-unit>
<trans-unit id="s13b4cf044a01dde2">
<source>Then download the configuration to deploy the authentik Agent</source>
</trans-unit>
<trans-unit id="s12f523a52b843ea2">
<source>macOS</source>
</trans-unit>
@@ -4211,12 +4208,6 @@ doesn't pass when either or both of the selected options are equal or above the
<trans-unit id="s94a50f1495fb5ccd">
<source>Disk encryption</source>
</trans-unit>
<trans-unit id="s76ea179414f2b2a5">
<source>Disk size</source>
</trans-unit>
<trans-unit id="s25e7a078391a3ec3">
<source>Disk usage</source>
</trans-unit>
<trans-unit id="s416211a967a6db4e">
<source>Users / Groups</source>
</trans-unit>
@@ -9458,9 +9449,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="s6df8326edea3b23d">
<source>If no device was provided, this stage will stop flow execution.</source>
</trans-unit>
<trans-unit id="s506c7d2e87f6770e">
<source>Filename can only contain letters, numbers, dots, hyphens, and underscores</source>
</trans-unit>
<trans-unit id="s3a3d5b2575cd32ea">
<source>File uploaded successfully</source>
</trans-unit>
@@ -9593,9 +9581,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="sc47f8ab6162bb2bb">
<source>Outpost configuration</source>
</trans-unit>
<trans-unit id="s184c3b30bebb2dd8">
<source>Assign role permissions</source>
</trans-unit>
<trans-unit id="s11a2ac9f2bd811d8">
<source>Delete Object Permission</source>
</trans-unit>
@@ -9809,6 +9794,41 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="s46a03121a2c260ea">
<source>Buffer PolicyAccessView requests</source>
</trans-unit>
<trans-unit id="s361e9d929ee925e6">
<source>Assign Object Permission</source>
</trans-unit>
<trans-unit id="s5f6ad947b4824e40">
<source>Next, download the configuration to deploy the authentik Agent via MDM</source>
</trans-unit>
<trans-unit id="s523337424b694d5c">
<source>Device Access Group</source>
</trans-unit>
<trans-unit id="s7642bf28cf8f476c">
<source>Select a device access group to be added to upon enrollment.</source>
</trans-unit>
<trans-unit id="h30bbf18fbf87fa57">
<source>To create a data export, navigate to
<x id="0" equiv-text="&lt;a href=&quot;#/identity/users&quot;&gt;"/>Directory &gt; Users<x id="1" equiv-text="&lt;/a&gt;"/> or to
<x id="2" equiv-text="&lt;a href=&quot;#/events/log&quot;&gt;"/>Events &gt; Logs<x id="3" equiv-text="&lt;/a&gt;"/>.</source>
</trans-unit>
<trans-unit id="s813faec5ff1d32d1">
<source>Filename can only contain letters, numbers, dots, hyphens, underscores, and slashes</source>
</trans-unit>
<trans-unit id="s54af3ec70642782c">
<source>Choose the object permissions that you want the selected role to have on this object. These object permissions are in addition to any global permissions already within the role.</source>
</trans-unit>
<trans-unit id="sb3be65525dd1f92c">
<source>Assign object permissions to role</source>
</trans-unit>
<trans-unit id="sbcd108b66363075c">
<source>Device access group</source>
</trans-unit>
<trans-unit id="s325acae04cdcac57">
<source>Primary disk size</source>
</trans-unit>
<trans-unit id="sb4a957846c89fca1">
<source>Primary disk usage</source>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -4284,9 +4284,6 @@ doesn't pass when either or both of the selected options are equal or above the
<trans-unit id="sf96a86df0756bc7b">
<source>Afterwards, select the enrollment token you want to use:</source>
</trans-unit>
<trans-unit id="s13b4cf044a01dde2">
<source>Then download the configuration to deploy the authentik Agent</source>
</trans-unit>
<trans-unit id="s12f523a52b843ea2">
<source>macOS</source>
</trans-unit>
@@ -4385,12 +4382,6 @@ doesn't pass when either or both of the selected options are equal or above the
<trans-unit id="s94a50f1495fb5ccd">
<source>Disk encryption</source>
</trans-unit>
<trans-unit id="s76ea179414f2b2a5">
<source>Disk size</source>
</trans-unit>
<trans-unit id="s25e7a078391a3ec3">
<source>Disk usage</source>
</trans-unit>
<trans-unit id="s416211a967a6db4e">
<source>Users / Groups</source>
</trans-unit>
@@ -9752,9 +9743,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="s6df8326edea3b23d">
<source>If no device was provided, this stage will stop flow execution.</source>
</trans-unit>
<trans-unit id="s506c7d2e87f6770e">
<source>Filename can only contain letters, numbers, dots, hyphens, and underscores</source>
</trans-unit>
<trans-unit id="s3a3d5b2575cd32ea">
<source>File uploaded successfully</source>
</trans-unit>
@@ -9887,9 +9875,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="sc47f8ab6162bb2bb">
<source>Outpost configuration</source>
</trans-unit>
<trans-unit id="s184c3b30bebb2dd8">
<source>Assign role permissions</source>
</trans-unit>
<trans-unit id="s11a2ac9f2bd811d8">
<source>Delete Object Permission</source>
</trans-unit>
@@ -10103,6 +10088,41 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="s46a03121a2c260ea">
<source>Buffer PolicyAccessView requests</source>
</trans-unit>
<trans-unit id="s361e9d929ee925e6">
<source>Assign Object Permission</source>
</trans-unit>
<trans-unit id="s5f6ad947b4824e40">
<source>Next, download the configuration to deploy the authentik Agent via MDM</source>
</trans-unit>
<trans-unit id="s523337424b694d5c">
<source>Device Access Group</source>
</trans-unit>
<trans-unit id="s7642bf28cf8f476c">
<source>Select a device access group to be added to upon enrollment.</source>
</trans-unit>
<trans-unit id="h30bbf18fbf87fa57">
<source>To create a data export, navigate to
<x id="0" equiv-text="&lt;a href=&quot;#/identity/users&quot;&gt;"/>Directory &gt; Users<x id="1" equiv-text="&lt;/a&gt;"/> or to
<x id="2" equiv-text="&lt;a href=&quot;#/events/log&quot;&gt;"/>Events &gt; Logs<x id="3" equiv-text="&lt;/a&gt;"/>.</source>
</trans-unit>
<trans-unit id="s813faec5ff1d32d1">
<source>Filename can only contain letters, numbers, dots, hyphens, underscores, and slashes</source>
</trans-unit>
<trans-unit id="s54af3ec70642782c">
<source>Choose the object permissions that you want the selected role to have on this object. These object permissions are in addition to any global permissions already within the role.</source>
</trans-unit>
<trans-unit id="sb3be65525dd1f92c">
<source>Assign object permissions to role</source>
</trans-unit>
<trans-unit id="sbcd108b66363075c">
<source>Device access group</source>
</trans-unit>
<trans-unit id="s325acae04cdcac57">
<source>Primary disk size</source>
</trans-unit>
<trans-unit id="sb4a957846c89fca1">
<source>Primary disk usage</source>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -3951,9 +3951,6 @@ doesn't pass when either or both of the selected options are equal or above the
<trans-unit id="sf96a86df0756bc7b">
<source>Afterwards, select the enrollment token you want to use:</source>
</trans-unit>
<trans-unit id="s13b4cf044a01dde2">
<source>Then download the configuration to deploy the authentik Agent</source>
</trans-unit>
<trans-unit id="s12f523a52b843ea2">
<source>macOS</source>
</trans-unit>
@@ -4052,12 +4049,6 @@ doesn't pass when either or both of the selected options are equal or above the
<trans-unit id="s94a50f1495fb5ccd">
<source>Disk encryption</source>
</trans-unit>
<trans-unit id="s76ea179414f2b2a5">
<source>Disk size</source>
</trans-unit>
<trans-unit id="s25e7a078391a3ec3">
<source>Disk usage</source>
</trans-unit>
<trans-unit id="s416211a967a6db4e">
<source>Users / Groups</source>
</trans-unit>
@@ -9083,9 +9074,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="s6df8326edea3b23d">
<source>If no device was provided, this stage will stop flow execution.</source>
</trans-unit>
<trans-unit id="s506c7d2e87f6770e">
<source>Filename can only contain letters, numbers, dots, hyphens, and underscores</source>
</trans-unit>
<trans-unit id="s3a3d5b2575cd32ea">
<source>File uploaded successfully</source>
</trans-unit>
@@ -9218,9 +9206,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="sc47f8ab6162bb2bb">
<source>Outpost configuration</source>
</trans-unit>
<trans-unit id="s184c3b30bebb2dd8">
<source>Assign role permissions</source>
</trans-unit>
<trans-unit id="s11a2ac9f2bd811d8">
<source>Delete Object Permission</source>
</trans-unit>
@@ -9434,6 +9419,41 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="s46a03121a2c260ea">
<source>Buffer PolicyAccessView requests</source>
</trans-unit>
<trans-unit id="s361e9d929ee925e6">
<source>Assign Object Permission</source>
</trans-unit>
<trans-unit id="s5f6ad947b4824e40">
<source>Next, download the configuration to deploy the authentik Agent via MDM</source>
</trans-unit>
<trans-unit id="s523337424b694d5c">
<source>Device Access Group</source>
</trans-unit>
<trans-unit id="s7642bf28cf8f476c">
<source>Select a device access group to be added to upon enrollment.</source>
</trans-unit>
<trans-unit id="h30bbf18fbf87fa57">
<source>To create a data export, navigate to
<x id="0" equiv-text="&lt;a href=&quot;#/identity/users&quot;&gt;"/>Directory &gt; Users<x id="1" equiv-text="&lt;/a&gt;"/> or to
<x id="2" equiv-text="&lt;a href=&quot;#/events/log&quot;&gt;"/>Events &gt; Logs<x id="3" equiv-text="&lt;/a&gt;"/>.</source>
</trans-unit>
<trans-unit id="s813faec5ff1d32d1">
<source>Filename can only contain letters, numbers, dots, hyphens, underscores, and slashes</source>
</trans-unit>
<trans-unit id="s54af3ec70642782c">
<source>Choose the object permissions that you want the selected role to have on this object. These object permissions are in addition to any global permissions already within the role.</source>
</trans-unit>
<trans-unit id="sb3be65525dd1f92c">
<source>Assign object permissions to role</source>
</trans-unit>
<trans-unit id="sbcd108b66363075c">
<source>Device access group</source>
</trans-unit>
<trans-unit id="s325acae04cdcac57">
<source>Primary disk size</source>
</trans-unit>
<trans-unit id="sb4a957846c89fca1">
<source>Primary disk usage</source>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -3789,9 +3789,6 @@ slaagt niet wanneer een of beide geselecteerde opties gelijk zijn aan of boven d
<trans-unit id="sf96a86df0756bc7b">
<source>Afterwards, select the enrollment token you want to use:</source>
</trans-unit>
<trans-unit id="s13b4cf044a01dde2">
<source>Then download the configuration to deploy the authentik Agent</source>
</trans-unit>
<trans-unit id="s12f523a52b843ea2">
<source>macOS</source>
</trans-unit>
@@ -3890,12 +3887,6 @@ slaagt niet wanneer een of beide geselecteerde opties gelijk zijn aan of boven d
<trans-unit id="s94a50f1495fb5ccd">
<source>Disk encryption</source>
</trans-unit>
<trans-unit id="s76ea179414f2b2a5">
<source>Disk size</source>
</trans-unit>
<trans-unit id="s25e7a078391a3ec3">
<source>Disk usage</source>
</trans-unit>
<trans-unit id="s416211a967a6db4e">
<source>Users / Groups</source>
</trans-unit>
@@ -8731,9 +8722,6 @@ Bindingen naar groepen/gebruikers worden gecontroleerd tegen de gebruiker van de
<trans-unit id="s6df8326edea3b23d">
<source>If no device was provided, this stage will stop flow execution.</source>
</trans-unit>
<trans-unit id="s506c7d2e87f6770e">
<source>Filename can only contain letters, numbers, dots, hyphens, and underscores</source>
</trans-unit>
<trans-unit id="s3a3d5b2575cd32ea">
<source>File uploaded successfully</source>
</trans-unit>
@@ -8866,9 +8854,6 @@ Bindingen naar groepen/gebruikers worden gecontroleerd tegen de gebruiker van de
<trans-unit id="sc47f8ab6162bb2bb">
<source>Outpost configuration</source>
</trans-unit>
<trans-unit id="s184c3b30bebb2dd8">
<source>Assign role permissions</source>
</trans-unit>
<trans-unit id="s11a2ac9f2bd811d8">
<source>Delete Object Permission</source>
</trans-unit>
@@ -9082,6 +9067,41 @@ Bindingen naar groepen/gebruikers worden gecontroleerd tegen de gebruiker van de
<trans-unit id="s46a03121a2c260ea">
<source>Buffer PolicyAccessView requests</source>
</trans-unit>
<trans-unit id="s361e9d929ee925e6">
<source>Assign Object Permission</source>
</trans-unit>
<trans-unit id="s5f6ad947b4824e40">
<source>Next, download the configuration to deploy the authentik Agent via MDM</source>
</trans-unit>
<trans-unit id="s523337424b694d5c">
<source>Device Access Group</source>
</trans-unit>
<trans-unit id="s7642bf28cf8f476c">
<source>Select a device access group to be added to upon enrollment.</source>
</trans-unit>
<trans-unit id="h30bbf18fbf87fa57">
<source>To create a data export, navigate to
<x id="0" equiv-text="&lt;a href=&quot;#/identity/users&quot;&gt;"/>Directory &gt; Users<x id="1" equiv-text="&lt;/a&gt;"/> or to
<x id="2" equiv-text="&lt;a href=&quot;#/events/log&quot;&gt;"/>Events &gt; Logs<x id="3" equiv-text="&lt;/a&gt;"/>.</source>
</trans-unit>
<trans-unit id="s813faec5ff1d32d1">
<source>Filename can only contain letters, numbers, dots, hyphens, underscores, and slashes</source>
</trans-unit>
<trans-unit id="s54af3ec70642782c">
<source>Choose the object permissions that you want the selected role to have on this object. These object permissions are in addition to any global permissions already within the role.</source>
</trans-unit>
<trans-unit id="sb3be65525dd1f92c">
<source>Assign object permissions to role</source>
</trans-unit>
<trans-unit id="sbcd108b66363075c">
<source>Device access group</source>
</trans-unit>
<trans-unit id="s325acae04cdcac57">
<source>Primary disk size</source>
</trans-unit>
<trans-unit id="sb4a957846c89fca1">
<source>Primary disk usage</source>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -3966,9 +3966,6 @@ nie przechodzi, gdy jedna lub obie wybrane opcje są równe lub wyższe od progu
<trans-unit id="sf96a86df0756bc7b">
<source>Afterwards, select the enrollment token you want to use:</source>
</trans-unit>
<trans-unit id="s13b4cf044a01dde2">
<source>Then download the configuration to deploy the authentik Agent</source>
</trans-unit>
<trans-unit id="s12f523a52b843ea2">
<source>macOS</source>
</trans-unit>
@@ -4067,12 +4064,6 @@ nie przechodzi, gdy jedna lub obie wybrane opcje są równe lub wyższe od progu
<trans-unit id="s94a50f1495fb5ccd">
<source>Disk encryption</source>
</trans-unit>
<trans-unit id="s76ea179414f2b2a5">
<source>Disk size</source>
</trans-unit>
<trans-unit id="s25e7a078391a3ec3">
<source>Disk usage</source>
</trans-unit>
<trans-unit id="s416211a967a6db4e">
<source>Users / Groups</source>
</trans-unit>
@@ -9113,9 +9104,6 @@ Powiązania z grupami/użytkownikami są sprawdzane względem użytkownika zdarz
<trans-unit id="s6df8326edea3b23d">
<source>If no device was provided, this stage will stop flow execution.</source>
</trans-unit>
<trans-unit id="s506c7d2e87f6770e">
<source>Filename can only contain letters, numbers, dots, hyphens, and underscores</source>
</trans-unit>
<trans-unit id="s3a3d5b2575cd32ea">
<source>File uploaded successfully</source>
</trans-unit>
@@ -9248,9 +9236,6 @@ Powiązania z grupami/użytkownikami są sprawdzane względem użytkownika zdarz
<trans-unit id="sc47f8ab6162bb2bb">
<source>Outpost configuration</source>
</trans-unit>
<trans-unit id="s184c3b30bebb2dd8">
<source>Assign role permissions</source>
</trans-unit>
<trans-unit id="s11a2ac9f2bd811d8">
<source>Delete Object Permission</source>
</trans-unit>
@@ -9464,6 +9449,41 @@ Powiązania z grupami/użytkownikami są sprawdzane względem użytkownika zdarz
<trans-unit id="s46a03121a2c260ea">
<source>Buffer PolicyAccessView requests</source>
</trans-unit>
<trans-unit id="s361e9d929ee925e6">
<source>Assign Object Permission</source>
</trans-unit>
<trans-unit id="s5f6ad947b4824e40">
<source>Next, download the configuration to deploy the authentik Agent via MDM</source>
</trans-unit>
<trans-unit id="s523337424b694d5c">
<source>Device Access Group</source>
</trans-unit>
<trans-unit id="s7642bf28cf8f476c">
<source>Select a device access group to be added to upon enrollment.</source>
</trans-unit>
<trans-unit id="h30bbf18fbf87fa57">
<source>To create a data export, navigate to
<x id="0" equiv-text="&lt;a href=&quot;#/identity/users&quot;&gt;"/>Directory &gt; Users<x id="1" equiv-text="&lt;/a&gt;"/> or to
<x id="2" equiv-text="&lt;a href=&quot;#/events/log&quot;&gt;"/>Events &gt; Logs<x id="3" equiv-text="&lt;/a&gt;"/>.</source>
</trans-unit>
<trans-unit id="s813faec5ff1d32d1">
<source>Filename can only contain letters, numbers, dots, hyphens, underscores, and slashes</source>
</trans-unit>
<trans-unit id="s54af3ec70642782c">
<source>Choose the object permissions that you want the selected role to have on this object. These object permissions are in addition to any global permissions already within the role.</source>
</trans-unit>
<trans-unit id="sb3be65525dd1f92c">
<source>Assign object permissions to role</source>
</trans-unit>
<trans-unit id="sbcd108b66363075c">
<source>Device access group</source>
</trans-unit>
<trans-unit id="s325acae04cdcac57">
<source>Primary disk size</source>
</trans-unit>
<trans-unit id="sb4a957846c89fca1">
<source>Primary disk usage</source>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -4287,9 +4287,6 @@ doesn't pass when either or both of the selected options are equal or above the
<trans-unit id="sf96a86df0756bc7b">
<source>Afterwards, select the enrollment token you want to use:</source>
</trans-unit>
<trans-unit id="s13b4cf044a01dde2">
<source>Then download the configuration to deploy the authentik Agent</source>
</trans-unit>
<trans-unit id="s12f523a52b843ea2">
<source>macOS</source>
</trans-unit>
@@ -4388,12 +4385,6 @@ doesn't pass when either or both of the selected options are equal or above the
<trans-unit id="s94a50f1495fb5ccd">
<source>Disk encryption</source>
</trans-unit>
<trans-unit id="s76ea179414f2b2a5">
<source>Disk size</source>
</trans-unit>
<trans-unit id="s25e7a078391a3ec3">
<source>Disk usage</source>
</trans-unit>
<trans-unit id="s416211a967a6db4e">
<source>Users / Groups</source>
</trans-unit>
@@ -9741,9 +9732,6 @@ por exemplo: <x id="0" equiv-text="&lt;code&gt;"/>oci://registry.domain.tld/path
<trans-unit id="s6df8326edea3b23d">
<source>If no device was provided, this stage will stop flow execution.</source>
</trans-unit>
<trans-unit id="s506c7d2e87f6770e">
<source>Filename can only contain letters, numbers, dots, hyphens, and underscores</source>
</trans-unit>
<trans-unit id="s3a3d5b2575cd32ea">
<source>File uploaded successfully</source>
</trans-unit>
@@ -9876,9 +9864,6 @@ por exemplo: <x id="0" equiv-text="&lt;code&gt;"/>oci://registry.domain.tld/path
<trans-unit id="sc47f8ab6162bb2bb">
<source>Outpost configuration</source>
</trans-unit>
<trans-unit id="s184c3b30bebb2dd8">
<source>Assign role permissions</source>
</trans-unit>
<trans-unit id="s11a2ac9f2bd811d8">
<source>Delete Object Permission</source>
</trans-unit>
@@ -10092,6 +10077,41 @@ por exemplo: <x id="0" equiv-text="&lt;code&gt;"/>oci://registry.domain.tld/path
<trans-unit id="s46a03121a2c260ea">
<source>Buffer PolicyAccessView requests</source>
</trans-unit>
<trans-unit id="s361e9d929ee925e6">
<source>Assign Object Permission</source>
</trans-unit>
<trans-unit id="s5f6ad947b4824e40">
<source>Next, download the configuration to deploy the authentik Agent via MDM</source>
</trans-unit>
<trans-unit id="s523337424b694d5c">
<source>Device Access Group</source>
</trans-unit>
<trans-unit id="s7642bf28cf8f476c">
<source>Select a device access group to be added to upon enrollment.</source>
</trans-unit>
<trans-unit id="h30bbf18fbf87fa57">
<source>To create a data export, navigate to
<x id="0" equiv-text="&lt;a href=&quot;#/identity/users&quot;&gt;"/>Directory &gt; Users<x id="1" equiv-text="&lt;/a&gt;"/> or to
<x id="2" equiv-text="&lt;a href=&quot;#/events/log&quot;&gt;"/>Events &gt; Logs<x id="3" equiv-text="&lt;/a&gt;"/>.</source>
</trans-unit>
<trans-unit id="s813faec5ff1d32d1">
<source>Filename can only contain letters, numbers, dots, hyphens, underscores, and slashes</source>
</trans-unit>
<trans-unit id="s54af3ec70642782c">
<source>Choose the object permissions that you want the selected role to have on this object. These object permissions are in addition to any global permissions already within the role.</source>
</trans-unit>
<trans-unit id="sb3be65525dd1f92c">
<source>Assign object permissions to role</source>
</trans-unit>
<trans-unit id="sbcd108b66363075c">
<source>Device access group</source>
</trans-unit>
<trans-unit id="s325acae04cdcac57">
<source>Primary disk size</source>
</trans-unit>
<trans-unit id="sb4a957846c89fca1">
<source>Primary disk usage</source>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -4004,9 +4004,6 @@ doesn't pass when either or both of the selected options are equal or above the
<trans-unit id="sf96a86df0756bc7b">
<source>Afterwards, select the enrollment token you want to use:</source>
</trans-unit>
<trans-unit id="s13b4cf044a01dde2">
<source>Then download the configuration to deploy the authentik Agent</source>
</trans-unit>
<trans-unit id="s12f523a52b843ea2">
<source>macOS</source>
</trans-unit>
@@ -4105,12 +4102,6 @@ doesn't pass when either or both of the selected options are equal or above the
<trans-unit id="s94a50f1495fb5ccd">
<source>Disk encryption</source>
</trans-unit>
<trans-unit id="s76ea179414f2b2a5">
<source>Disk size</source>
</trans-unit>
<trans-unit id="s25e7a078391a3ec3">
<source>Disk usage</source>
</trans-unit>
<trans-unit id="s416211a967a6db4e">
<source>Users / Groups</source>
</trans-unit>
@@ -9201,9 +9192,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="s6df8326edea3b23d">
<source>If no device was provided, this stage will stop flow execution.</source>
</trans-unit>
<trans-unit id="s506c7d2e87f6770e">
<source>Filename can only contain letters, numbers, dots, hyphens, and underscores</source>
</trans-unit>
<trans-unit id="s3a3d5b2575cd32ea">
<source>File uploaded successfully</source>
</trans-unit>
@@ -9336,9 +9324,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="sc47f8ab6162bb2bb">
<source>Outpost configuration</source>
</trans-unit>
<trans-unit id="s184c3b30bebb2dd8">
<source>Assign role permissions</source>
</trans-unit>
<trans-unit id="s11a2ac9f2bd811d8">
<source>Delete Object Permission</source>
</trans-unit>
@@ -9552,6 +9537,41 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="s46a03121a2c260ea">
<source>Buffer PolicyAccessView requests</source>
</trans-unit>
<trans-unit id="s361e9d929ee925e6">
<source>Assign Object Permission</source>
</trans-unit>
<trans-unit id="s5f6ad947b4824e40">
<source>Next, download the configuration to deploy the authentik Agent via MDM</source>
</trans-unit>
<trans-unit id="s523337424b694d5c">
<source>Device Access Group</source>
</trans-unit>
<trans-unit id="s7642bf28cf8f476c">
<source>Select a device access group to be added to upon enrollment.</source>
</trans-unit>
<trans-unit id="h30bbf18fbf87fa57">
<source>To create a data export, navigate to
<x id="0" equiv-text="&lt;a href=&quot;#/identity/users&quot;&gt;"/>Directory &gt; Users<x id="1" equiv-text="&lt;/a&gt;"/> or to
<x id="2" equiv-text="&lt;a href=&quot;#/events/log&quot;&gt;"/>Events &gt; Logs<x id="3" equiv-text="&lt;/a&gt;"/>.</source>
</trans-unit>
<trans-unit id="s813faec5ff1d32d1">
<source>Filename can only contain letters, numbers, dots, hyphens, underscores, and slashes</source>
</trans-unit>
<trans-unit id="s54af3ec70642782c">
<source>Choose the object permissions that you want the selected role to have on this object. These object permissions are in addition to any global permissions already within the role.</source>
</trans-unit>
<trans-unit id="sb3be65525dd1f92c">
<source>Assign object permissions to role</source>
</trans-unit>
<trans-unit id="sbcd108b66363075c">
<source>Device access group</source>
</trans-unit>
<trans-unit id="s325acae04cdcac57">
<source>Primary disk size</source>
</trans-unit>
<trans-unit id="sb4a957846c89fca1">
<source>Primary disk usage</source>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -3983,9 +3983,6 @@ Belirlenen seçeneklerden biri veya her ikisi de eşiğe eşit veya eşiğin üz
<trans-unit id="sf96a86df0756bc7b">
<source>Afterwards, select the enrollment token you want to use:</source>
</trans-unit>
<trans-unit id="s13b4cf044a01dde2">
<source>Then download the configuration to deploy the authentik Agent</source>
</trans-unit>
<trans-unit id="s12f523a52b843ea2">
<source>macOS</source>
</trans-unit>
@@ -4084,12 +4081,6 @@ Belirlenen seçeneklerden biri veya her ikisi de eşiğe eşit veya eşiğin üz
<trans-unit id="s94a50f1495fb5ccd">
<source>Disk encryption</source>
</trans-unit>
<trans-unit id="s76ea179414f2b2a5">
<source>Disk size</source>
</trans-unit>
<trans-unit id="s25e7a078391a3ec3">
<source>Disk usage</source>
</trans-unit>
<trans-unit id="s416211a967a6db4e">
<source>Users / Groups</source>
</trans-unit>
@@ -9179,9 +9170,6 @@ Gruplara/kullanıcılara yapılan bağlamalar, etkinliğin kullanıcısına kar
<trans-unit id="s6df8326edea3b23d">
<source>If no device was provided, this stage will stop flow execution.</source>
</trans-unit>
<trans-unit id="s506c7d2e87f6770e">
<source>Filename can only contain letters, numbers, dots, hyphens, and underscores</source>
</trans-unit>
<trans-unit id="s3a3d5b2575cd32ea">
<source>File uploaded successfully</source>
</trans-unit>
@@ -9314,9 +9302,6 @@ Gruplara/kullanıcılara yapılan bağlamalar, etkinliğin kullanıcısına kar
<trans-unit id="sc47f8ab6162bb2bb">
<source>Outpost configuration</source>
</trans-unit>
<trans-unit id="s184c3b30bebb2dd8">
<source>Assign role permissions</source>
</trans-unit>
<trans-unit id="s11a2ac9f2bd811d8">
<source>Delete Object Permission</source>
</trans-unit>
@@ -9530,6 +9515,41 @@ Gruplara/kullanıcılara yapılan bağlamalar, etkinliğin kullanıcısına kar
<trans-unit id="s46a03121a2c260ea">
<source>Buffer PolicyAccessView requests</source>
</trans-unit>
<trans-unit id="s361e9d929ee925e6">
<source>Assign Object Permission</source>
</trans-unit>
<trans-unit id="s5f6ad947b4824e40">
<source>Next, download the configuration to deploy the authentik Agent via MDM</source>
</trans-unit>
<trans-unit id="s523337424b694d5c">
<source>Device Access Group</source>
</trans-unit>
<trans-unit id="s7642bf28cf8f476c">
<source>Select a device access group to be added to upon enrollment.</source>
</trans-unit>
<trans-unit id="h30bbf18fbf87fa57">
<source>To create a data export, navigate to
<x id="0" equiv-text="&lt;a href=&quot;#/identity/users&quot;&gt;"/>Directory &gt; Users<x id="1" equiv-text="&lt;/a&gt;"/> or to
<x id="2" equiv-text="&lt;a href=&quot;#/events/log&quot;&gt;"/>Events &gt; Logs<x id="3" equiv-text="&lt;/a&gt;"/>.</source>
</trans-unit>
<trans-unit id="s813faec5ff1d32d1">
<source>Filename can only contain letters, numbers, dots, hyphens, underscores, and slashes</source>
</trans-unit>
<trans-unit id="s54af3ec70642782c">
<source>Choose the object permissions that you want the selected role to have on this object. These object permissions are in addition to any global permissions already within the role.</source>
</trans-unit>
<trans-unit id="sb3be65525dd1f92c">
<source>Assign object permissions to role</source>
</trans-unit>
<trans-unit id="sbcd108b66363075c">
<source>Device access group</source>
</trans-unit>
<trans-unit id="s325acae04cdcac57">
<source>Primary disk size</source>
</trans-unit>
<trans-unit id="sb4a957846c89fca1">
<source>Primary disk usage</source>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -4250,9 +4250,6 @@ doesn't pass when either or both of the selected options are equal or above the
<trans-unit id="sf96a86df0756bc7b">
<source>Afterwards, select the enrollment token you want to use:</source>
</trans-unit>
<trans-unit id="s13b4cf044a01dde2">
<source>Then download the configuration to deploy the authentik Agent</source>
</trans-unit>
<trans-unit id="s12f523a52b843ea2">
<source>macOS</source>
</trans-unit>
@@ -4351,12 +4348,6 @@ doesn't pass when either or both of the selected options are equal or above the
<trans-unit id="s94a50f1495fb5ccd">
<source>Disk encryption</source>
</trans-unit>
<trans-unit id="s76ea179414f2b2a5">
<source>Disk size</source>
</trans-unit>
<trans-unit id="s25e7a078391a3ec3">
<source>Disk usage</source>
</trans-unit>
<trans-unit id="s416211a967a6db4e">
<source>Users / Groups</source>
</trans-unit>
@@ -9713,9 +9704,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="s6df8326edea3b23d">
<source>If no device was provided, this stage will stop flow execution.</source>
</trans-unit>
<trans-unit id="s506c7d2e87f6770e">
<source>Filename can only contain letters, numbers, dots, hyphens, and underscores</source>
</trans-unit>
<trans-unit id="s3a3d5b2575cd32ea">
<source>File uploaded successfully</source>
</trans-unit>
@@ -9848,9 +9836,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="sc47f8ab6162bb2bb">
<source>Outpost configuration</source>
</trans-unit>
<trans-unit id="s184c3b30bebb2dd8">
<source>Assign role permissions</source>
</trans-unit>
<trans-unit id="s11a2ac9f2bd811d8">
<source>Delete Object Permission</source>
</trans-unit>
@@ -10064,6 +10049,41 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="s46a03121a2c260ea">
<source>Buffer PolicyAccessView requests</source>
</trans-unit>
<trans-unit id="s361e9d929ee925e6">
<source>Assign Object Permission</source>
</trans-unit>
<trans-unit id="s5f6ad947b4824e40">
<source>Next, download the configuration to deploy the authentik Agent via MDM</source>
</trans-unit>
<trans-unit id="s523337424b694d5c">
<source>Device Access Group</source>
</trans-unit>
<trans-unit id="s7642bf28cf8f476c">
<source>Select a device access group to be added to upon enrollment.</source>
</trans-unit>
<trans-unit id="h30bbf18fbf87fa57">
<source>To create a data export, navigate to
<x id="0" equiv-text="&lt;a href=&quot;#/identity/users&quot;&gt;"/>Directory &gt; Users<x id="1" equiv-text="&lt;/a&gt;"/> or to
<x id="2" equiv-text="&lt;a href=&quot;#/events/log&quot;&gt;"/>Events &gt; Logs<x id="3" equiv-text="&lt;/a&gt;"/>.</source>
</trans-unit>
<trans-unit id="s813faec5ff1d32d1">
<source>Filename can only contain letters, numbers, dots, hyphens, underscores, and slashes</source>
</trans-unit>
<trans-unit id="s54af3ec70642782c">
<source>Choose the object permissions that you want the selected role to have on this object. These object permissions are in addition to any global permissions already within the role.</source>
</trans-unit>
<trans-unit id="sb3be65525dd1f92c">
<source>Assign object permissions to role</source>
</trans-unit>
<trans-unit id="sbcd108b66363075c">
<source>Device access group</source>
</trans-unit>
<trans-unit id="s325acae04cdcac57">
<source>Primary disk size</source>
</trans-unit>
<trans-unit id="sb4a957846c89fca1">
<source>Primary disk usage</source>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -3821,9 +3821,6 @@ doesn't pass when either or both of the selected options are equal or above the
<trans-unit id="sf96a86df0756bc7b">
<source>Afterwards, select the enrollment token you want to use:</source>
</trans-unit>
<trans-unit id="s13b4cf044a01dde2">
<source>Then download the configuration to deploy the authentik Agent</source>
</trans-unit>
<trans-unit id="s12f523a52b843ea2">
<source>macOS</source>
</trans-unit>
@@ -3922,12 +3919,6 @@ doesn't pass when either or both of the selected options are equal or above the
<trans-unit id="s94a50f1495fb5ccd">
<source>Disk encryption</source>
</trans-unit>
<trans-unit id="s76ea179414f2b2a5">
<source>Disk size</source>
</trans-unit>
<trans-unit id="s25e7a078391a3ec3">
<source>Disk usage</source>
</trans-unit>
<trans-unit id="s416211a967a6db4e">
<source>Users / Groups</source>
</trans-unit>
@@ -8794,9 +8785,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="s6df8326edea3b23d">
<source>If no device was provided, this stage will stop flow execution.</source>
</trans-unit>
<trans-unit id="s506c7d2e87f6770e">
<source>Filename can only contain letters, numbers, dots, hyphens, and underscores</source>
</trans-unit>
<trans-unit id="s3a3d5b2575cd32ea">
<source>File uploaded successfully</source>
</trans-unit>
@@ -8929,9 +8917,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="sc47f8ab6162bb2bb">
<source>Outpost configuration</source>
</trans-unit>
<trans-unit id="s184c3b30bebb2dd8">
<source>Assign role permissions</source>
</trans-unit>
<trans-unit id="s11a2ac9f2bd811d8">
<source>Delete Object Permission</source>
</trans-unit>
@@ -9145,6 +9130,41 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="s46a03121a2c260ea">
<source>Buffer PolicyAccessView requests</source>
</trans-unit>
<trans-unit id="s361e9d929ee925e6">
<source>Assign Object Permission</source>
</trans-unit>
<trans-unit id="s5f6ad947b4824e40">
<source>Next, download the configuration to deploy the authentik Agent via MDM</source>
</trans-unit>
<trans-unit id="s523337424b694d5c">
<source>Device Access Group</source>
</trans-unit>
<trans-unit id="s7642bf28cf8f476c">
<source>Select a device access group to be added to upon enrollment.</source>
</trans-unit>
<trans-unit id="h30bbf18fbf87fa57">
<source>To create a data export, navigate to
<x id="0" equiv-text="&lt;a href=&quot;#/identity/users&quot;&gt;"/>Directory &gt; Users<x id="1" equiv-text="&lt;/a&gt;"/> or to
<x id="2" equiv-text="&lt;a href=&quot;#/events/log&quot;&gt;"/>Events &gt; Logs<x id="3" equiv-text="&lt;/a&gt;"/>.</source>
</trans-unit>
<trans-unit id="s813faec5ff1d32d1">
<source>Filename can only contain letters, numbers, dots, hyphens, underscores, and slashes</source>
</trans-unit>
<trans-unit id="s54af3ec70642782c">
<source>Choose the object permissions that you want the selected role to have on this object. These object permissions are in addition to any global permissions already within the role.</source>
</trans-unit>
<trans-unit id="sb3be65525dd1f92c">
<source>Assign object permissions to role</source>
</trans-unit>
<trans-unit id="sbcd108b66363075c">
<source>Device access group</source>
</trans-unit>
<trans-unit id="s325acae04cdcac57">
<source>Primary disk size</source>
</trans-unit>
<trans-unit id="sb4a957846c89fca1">
<source>Primary disk usage</source>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -36,6 +36,8 @@ Firefox has some known issues regarding TouchID (see https://bugzilla.mozilla.or
Passwordless authentication currently only supports WebAuthn devices, which provides for the use of passkeys, security keys and biometrics. For an alternate passwordless setup, see [Password stage](../password/index.md#passwordless-login), which supports other types.
If you want users to authenticate with a passkey via the browser's built-in passkey/autofill UI on the **Identification** screen ("conditional UI" / passkey autofill), configure it in the [Identification stage](../identification/index.mdx#passkey-autofill-webauthn-conditional-ui). This requires a **discoverable credential (aka resident key)**.
To configure passwordless authentication, create a new Flow with the designation set to _Authentication_.
As first stage, add an _Authenticator validation_ stage, with the WebAuthn device class allowed.

View File

@@ -26,6 +26,46 @@ The CAPTCHA stage you use must be configured to use the "Invisible" mode, otherw
To run a CAPTCHA process in the background while the user is entering their identification, a CAPTCHA stage can be selected here. If a CAPTCHA stage is selected in the Identification stage, the CAPTCHA stage should not be bound to the flow.
## Passkey autofill (WebAuthn conditional UI):ak-version[2025.12]
When configured, the Identification stage can offer passkey login directly from the browser's passkey/autofill UI (also known as "conditional UI"). This allows a user to select a passkey without first typing their username.
authentik will automatically fall back to the normal identification flow when passkey autofill is not available.
### Requirements
- **HTTPS** is required for WebAuthn (except on `localhost`).
- **Browser support** for WebAuthn conditional mediation is required.
- Users must have a compatible **discoverable credential (aka resident key)** (most passkeys created by platform authenticators and password managers are discoverable).
- **Correct domain**: users must access authentik using the same hostname the passkey was created for.
### Configuration
1. Log in to authentik as an administrator and open the authentik Admin interface.
2. Navigate to **Flows and Stages** > **Stages** and either create or edit an [Authenticator validation stage](../authenticator_validate/index.mdx) that allows the **WebAuthn** device class.
3. Navigate to **Flows and Stages** > **Stages** and edit your Identification stage. Under **Passkey settings** set **WebAuthn Authenticator Validation Stage** to the Authenticator validation stage from step 2.
4. Click **Update** to save the changes.
5. Ensure users have enrolled a passkey/WebAuthn device (for example using the [WebAuthn / FIDO2 / Passkeys Authenticator setup stage](../authenticator_webauthn/index.mdx)).
### Notes
- The passkey prompt is triggered by the browser when the user focuses the username field.
- If a user has multiple passkeys, the browser will show a picker.
- If passkey login is used, the flow context will have `auth_method` set to `auth_webauthn_pwl`.
- In the default authentication flow blueprint, authentik skips the MFA validation stage after passkey login using an expression policy. If you want passkey login to still require an additional factor, disable or adjust that policy binding on the MFA stage.
### Troubleshooting
- **No passkey prompt appears**
- Ensure the Identification stage has **WebAuthn Authenticator Validation Stage** set.
- Ensure you're using **HTTPS** (except on `localhost`).
- Check browser support for conditional UI.
- Ensure the login page is not embedded in an iframe as some browsers block conditional UI outside top-level browsing contexts.
- **Passkey prompt appears, but login falls back to username/password**
- Ensure the referenced Authenticator validation stage allows the **WebAuthn** device class.
- Ensure the user has a valid, confirmed WebAuthn device enrolled.
## Enrollment/Recovery Flow
These fields specify if and which flows are linked on the form. The enrollment flow is linked as `Need an account? Sign up.`, and the recovery flow is linked as `Forgot username or password?`.

Some files were not shown because too many files have changed in this diff Show More