Compare commits

...

29 Commits

Author SHA1 Message Date
a4f92f5e30 Prep for monorepo use.
web: Update config.

Flesh out build.

Fix issue surrounding build.

Fix paths.

Update workspaces.

Fix build steps.

Apply linter. Temporarily remove problem rules.

Add ignorefile. Prep for formatting.

Lint website.

Lint web, repo packages.

Refine Prettier usage. Fix imports.

Tidy build.

Move node ignore files.

Remove unused.

Update job. Fix lint step.

Build before compiling.

Use root for paths.

Fix issues surrounding import references, types, package names.

Fix build paths.

Tidy.

Enforce prefix.

Apply prefixes to imports.

Enable linter, compiler, etc.

Fix references. Update names.

Mark optional.

Revise mounts. Fix build order.

Update package.json.

Ignore all docusaurus.

Fix paths, types.

Clean up build steps, names.

Fix paths.

website: Fix nested paragraphs build warning.

web: Enforce module resolution.

Use consistent LTS version.

Track Node version.

Use default resolution.

Test main entrypoint.

Fix Node v20 compatibility.

Add task names.

WIP: Fix styles.
2025-04-17 02:46:10 +02:00
c15c0cbe86 website: integrations: apache guacamole: Fix deprecated start-of-doc … (#14114)
website: integrations: apache guacamole: Fix deprecated start-of-doc values
2025-04-16 15:45:44 -05:00
c6fe0c1d85 website integrations: actual budget: remove old header and support_level (#14112) 2025-04-16 15:45:20 -05:00
07f0666a6f website/integrations: general cleanup and updates (#12716)
* squash commits for future merge conflict resolution, if any

* adventurelog cleanup + lint

* lint (again)

* Update website/integrations/services/adventurelog/index.mdx

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

* Update website/integrations/services/actual-budget/index.mdx

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

* Update website/integrations/services/apache-guacamole/index.mdx

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

* Update website/integrations/services/gatus/index.mdx

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

* Update website/integrations/services/bookstack/index.mdx

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

* Update website/integrations/services/freshrss/index.mdx

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

* Update website/integrations/services/budibase/index.md

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

* Update website/integrations/services/cloudflare-access/index.md

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

* Update website/integrations/services/dokuwiki/index.md

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

* Update website/integrations/services/frappe/index.md

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

* Update website/integrations/services/espocrm/index.md

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

* Update website/integrations/services/fortimanager/index.md

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

* Update website/integrations/services/fortigate-admin/index.md

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

* Update website/integrations/services/firezone/index.md

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

* fix

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

* wip: migr actual budget integration to new codeblock

* Replaced multilinecodeblocks with docusaurus style codeblocks

* Fixed linting and removed kbd and em tags from codeblock

---------

Signed-off-by: Dominic R <dominic@sdko.org>
Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
2025-04-16 15:16:01 -05:00
51609d696d policies/geoip: fix result when only dynamic results are used (#14107) 2025-04-16 15:50:26 +00:00
c0d08df161 core: bump opentelemetry-api from 1.32.0 to v1.32.1 (#14102) 2025-04-16 15:50:10 +00:00
643a97f0a5 core: bump rsa from 4.9 to v4.9.1 (#14103) 2025-04-16 09:51:53 -04:00
155a31fd70 sources/oauth: introduce authorization code auth method (#14034)
Co-authored-by: Rsgm <rsgm123@gmail.com>
2025-04-16 13:00:08 +00:00
c6f9d5df7b core, web: update translations (#14096)
Co-authored-by: rissson <18313093+rissson@users.noreply.github.com>
2025-04-16 13:16:06 +02:00
ea85331a7e web/api: Fix Hoisted exports across entrypoints. Update Axios. (#14089)
* web/api: Fix issue where hoisted exports across entrypoints do not
order.

* web/api: Override OpenAPI transitive dep.
2025-04-15 20:09:41 +02:00
4f4c5253dd translate: Updates for file web/xliff/en.xlf in fr (#14091)
Translate web/xliff/en.xlf in fr

100% translated source file: 'web/xliff/en.xlf'
on 'fr'.

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-04-15 17:11:40 +00:00
83b2fc36df translate: Updates for file locale/en/LC_MESSAGES/django.po in fr (#14090)
Translate locale/en/LC_MESSAGES/django.po in fr

100% translated source file: 'locale/en/LC_MESSAGES/django.po'
on 'fr'.

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-04-15 17:08:09 +00:00
d99d2b8bdc translate: Updates for file locale/en/LC_MESSAGES/django.po in zh-Hans (#14087)
Translate django.po in zh-Hans

100% translated source file: 'django.po'
on 'zh-Hans'.

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-04-15 18:40:19 +02:00
9b96d04b3a translate: Updates for file web/xliff/en.xlf in zh-Hans (#14086)
Translate web/xliff/en.xlf in zh-Hans

100% translated source file: 'web/xliff/en.xlf'
on 'zh-Hans'.

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-04-15 18:40:02 +02:00
ca5b99eb16 translate: Updates for file web/xliff/en.xlf in zh_CN (#14084)
Translate web/xliff/en.xlf in zh_CN

100% translated source file: 'web/xliff/en.xlf'
on 'zh_CN'.

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-04-15 18:39:49 +02:00
4c1676e97c translate: Updates for file locale/en/LC_MESSAGES/django.po in zh_CN (#14083)
Translate locale/en/LC_MESSAGES/django.po in zh_CN

100% translated source file: 'locale/en/LC_MESSAGES/django.po'
on 'zh_CN'.

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-04-15 18:39:36 +02:00
81855cf2fe core: bump google-auth from 2.38.0 to v2.39.0 (#14076) 2025-04-15 08:32:54 -04:00
bd904027be core: bump sentry-sdk from 2.25.1 to v2.26.1 (#14079) 2025-04-15 08:32:14 -04:00
0ffc97db15 core: bump prompt-toolkit from 3.0.50 to v3.0.51 (#14078) 2025-04-15 08:31:41 -04:00
2c515b1e17 core: bump boto3 from 1.37.33 to v1.37.34 (#14074) 2025-04-15 08:31:12 -04:00
f8900fbaf3 core: bump msgraph-sdk from 1.27.0 to v1.28.0 (#14077) 2025-04-15 08:30:44 -04:00
0f4a98d9c6 website/docs: fix minor typo in working_with_policies.md (#14071)
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2025-04-15 11:40:23 +00:00
8853f25b45 core, web: update translations (#14064)
Co-authored-by: rissson <18313093+rissson@users.noreply.github.com>
2025-04-15 13:26:02 +02:00
1c40f7b95a stages/authenticator_webauthn: Update FIDO MDS3 & Passkey aaguid blobs (#14065)
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-04-15 13:25:13 +02:00
9b5d6ec1af core: bump goauthentik.io/api/v3 from 3.2025024.4 to 3.2025024.6 (#14069)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-15 13:24:28 +02:00
36d29a9ae1 Small fix for Actual-Budget wiki guide (#14066)
Remove ending slash from redirect uri

Signed-off-by: James Armstrong <32995055+jmarmstrong1207@users.noreply.github.com>
2025-04-15 09:43:59 +01:00
0606b1aba4 root: support db pool (#13534) 2025-04-14 16:05:31 +00:00
03d5dad867 rbac: add InitialPermissions (#13795)
* add `InitialPermissions` model to RBAC

This is a powerful construct between Permission and Role to set initial
permissions for newly created objects.

* use safer `request.user`

* fixup! use safer `request.user`

* force all self-defined serializers to descend from our custom one

See https://github.com/goauthentik/authentik/pull/10139

* reorganize initial permission assignment

* fixup! reorganize initial permission assignment
2025-04-14 17:55:49 +02:00
38a9e46af3 web: bump API Client version (#14058)
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-04-14 17:24:47 +02:00
325 changed files with 51319 additions and 55701 deletions

View File

@ -10,6 +10,9 @@ insert_final_newline = true
[*.html] [*.html]
indent_size = 2 indent_size = 2
[schemas/*.json]
indent_size = 2
[*.{yaml,yml}] [*.{yaml,yml}]
indent_size = 2 indent_size = 2

View File

@ -28,9 +28,9 @@ runs:
- name: Setup node - name: Setup node
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version-file: web/package.json node-version-file: package.json
cache: "npm" cache: "npm"
cache-dependency-path: web/package-lock.json cache-dependency-path: package-lock.json
- name: Setup go - name: Setup go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
@ -44,7 +44,7 @@ runs:
run: | run: |
export PSQL_TAG=${{ inputs.postgresql_version }} export PSQL_TAG=${{ inputs.postgresql_version }}
docker compose -f .github/actions/setup/docker-compose.yml up -d docker compose -f .github/actions/setup/docker-compose.yml up -d
cd web && npm ci npm ci
- name: Generate config - name: Generate config
shell: uv run python {0} shell: uv run python {0}
run: | run: |

View File

@ -1,5 +1,5 @@
# Re-usable workflow for a single-architecture build # Re-usable workflow for a single-architecture build
name: Single-arch Container build name: "Single-arch Container build"
on: on:
workflow_call: workflow_call:
@ -42,7 +42,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: docker/setup-qemu-action@v3.6.0 - uses: docker/setup-qemu-action@v3.6.0
- uses: docker/setup-buildx-action@v3 - uses: docker/setup-buildx-action@v3
- name: prepare variables - name: Prepare variables
uses: ./.github/actions/docker-push-variables uses: ./.github/actions/docker-push-variables
id: ev id: ev
env: env:
@ -64,12 +64,12 @@ jobs:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: make empty clients - name: Make empty clients
if: ${{ inputs.release }} if: ${{ inputs.release }}
run: | run: |
mkdir -p ./gen-ts-api mkdir -p ./gen-ts-api
mkdir -p ./gen-go-api mkdir -p ./gen-go-api
- name: generate ts client - name: Generate TypeScript API Client
if: ${{ !inputs.release }} if: ${{ !inputs.release }}
run: make gen-client-ts run: make gen-client-ts
- name: Build Docker Image - name: Build Docker Image

View File

@ -1,5 +1,5 @@
# Re-usable workflow for a multi-architecture build # Re-usable workflow for a multi-architecture build
name: Multi-arch container build name: "Multi-arch container build"
on: on:
workflow_call: workflow_call:
@ -49,7 +49,7 @@ jobs:
shouldPush: ${{ steps.ev.outputs.shouldPush }} shouldPush: ${{ steps.ev.outputs.shouldPush }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: prepare variables - name: Prepare variables
uses: ./.github/actions/docker-push-variables uses: ./.github/actions/docker-push-variables
id: ev id: ev
env: env:
@ -69,7 +69,7 @@ jobs:
tag: ${{ fromJson(needs.get-tags.outputs.tags) }} tag: ${{ fromJson(needs.get-tags.outputs.tags) }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: prepare variables - name: Prepare variables
uses: ./.github/actions/docker-push-variables uses: ./.github/actions/docker-push-variables
id: ev id: ev
env: env:

View File

@ -1,4 +1,5 @@
name: authentik-api-py-publish name: "Python API Publish"
on: on:
push: push:
branches: [main] branches: [main]
@ -7,6 +8,7 @@ on:
workflow_dispatch: workflow_dispatch:
jobs: jobs:
build: build:
name: "Build and Publish"
if: ${{ github.repository != 'goauthentik/authentik-internal' }} if: ${{ github.repository != 'goauthentik/authentik-internal' }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
@ -30,7 +32,7 @@ jobs:
uses: actions/setup-python@v5 uses: actions/setup-python@v5
with: with:
python-version-file: "pyproject.toml" python-version-file: "pyproject.toml"
- name: Generate API Client - name: Generate Python API Client
run: make gen-client-py run: make gen-client-py
- name: Publish package - name: Publish package
working-directory: gen-py-api/ working-directory: gen-py-api/

View File

@ -1,4 +1,4 @@
name: authentik-api-ts-publish name: "TypeScript API Publish"
on: on:
push: push:
branches: [main] branches: [main]
@ -7,6 +7,7 @@ on:
workflow_dispatch: workflow_dispatch:
jobs: jobs:
build: build:
name: "Build and Publish"
if: ${{ github.repository != 'goauthentik/authentik-internal' }} if: ${{ github.repository != 'goauthentik/authentik-internal' }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
@ -20,9 +21,9 @@ jobs:
token: ${{ steps.generate_token.outputs.token }} token: ${{ steps.generate_token.outputs.token }}
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version-file: web/package.json node-version-file: package.json
registry-url: "https://registry.npmjs.org" registry-url: "https://registry.npmjs.org"
- name: Generate API Client - name: Generate TypeScript API Client
run: make gen-client-ts run: make gen-client-ts
- name: Publish package - name: Publish package
working-directory: gen-ts-api/ working-directory: gen-ts-api/

View File

@ -1,4 +1,4 @@
name: authentik-ci-aws-cfn name: "authentik CI AWS CloudFormation"
on: on:
push: push:
@ -18,6 +18,7 @@ env:
jobs: jobs:
check-changes-applied: check-changes-applied:
name: "Check changes applied"
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@ -36,6 +37,7 @@ jobs:
uv run make aws-cfn uv run make aws-cfn
git diff --exit-code git diff --exit-code
ci-aws-cfn-mark: ci-aws-cfn-mark:
name: "CI AWS CloudFormation Mark"
if: always() if: always()
needs: needs:
- check-changes-applied - check-changes-applied

View File

@ -1,5 +1,5 @@
--- ---
name: authentik-ci-main-daily name: "authentik CI Main Daily"
on: on:
workflow_dispatch: workflow_dispatch:
@ -9,6 +9,7 @@ on:
jobs: jobs:
test-container: test-container:
name: "Test Container ${{ matrix.version }}"
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
fail-fast: false fail-fast: false

View File

@ -1,5 +1,5 @@
--- ---
name: authentik-ci-main name: "authentik CI Main"
on: on:
push: push:
@ -19,6 +19,7 @@ env:
jobs: jobs:
lint: lint:
name: "Lint"
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
@ -33,9 +34,10 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Setup authentik env - name: Setup authentik env
uses: ./.github/actions/setup uses: ./.github/actions/setup
- name: run job - name: Run job ${{ matrix.job }}
run: uv run make ci-${{ matrix.job }} run: uv run make ci-${{ matrix.job }}
test-migrations: test-migrations:
name: "Test Migrations"
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@ -44,6 +46,7 @@ jobs:
- name: run migrations - name: run migrations
run: uv run python -m lifecycle.migrate run: uv run python -m lifecycle.migrate
test-make-seed: test-make-seed:
name: "Test Make Seed"
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- id: seed - id: seed
@ -52,7 +55,7 @@ jobs:
outputs: outputs:
seed: ${{ steps.seed.outputs.seed }} seed: ${{ steps.seed.outputs.seed }}
test-migrations-from-stable: test-migrations-from-stable:
name: test-migrations-from-stable - PostgreSQL ${{ matrix.psql }} - Run ${{ matrix.run_id }}/5 name: "Test Migrations From Stable - PostgreSQL ${{ matrix.psql }} - Run ${{ matrix.run_id }}/5"
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 20 timeout-minutes: 20
needs: test-make-seed needs: test-make-seed
@ -67,7 +70,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: checkout stable - name: Checkout Stable
run: | run: |
# Copy current, latest config to local # Copy current, latest config to local
# Temporarly comment the .github backup while migrating to uv # Temporarly comment the .github backup while migrating to uv
@ -84,9 +87,9 @@ jobs:
with: with:
postgresql_version: ${{ matrix.psql }} postgresql_version: ${{ matrix.psql }}
continue-on-error: true continue-on-error: true
- name: run migrations to stable - name: Run migrations to stable
run: poetry run python -m lifecycle.migrate run: poetry run python -m lifecycle.migrate
- name: checkout current code - name: Checkout current code
run: | run: |
set -x set -x
git fetch git fetch
@ -97,10 +100,10 @@ jobs:
uses: ./.github/actions/setup uses: ./.github/actions/setup
with: with:
postgresql_version: ${{ matrix.psql }} postgresql_version: ${{ matrix.psql }}
- name: migrate to latest - name: Migrate to latest
run: | run: |
uv run python -m lifecycle.migrate uv run python -m lifecycle.migrate
- name: run tests - name: Run tests
env: env:
# Test in the main database that we just migrated from the previous stable version # Test in the main database that we just migrated from the previous stable version
AUTHENTIK_POSTGRESQL__TEST__NAME: authentik AUTHENTIK_POSTGRESQL__TEST__NAME: authentik
@ -110,7 +113,7 @@ jobs:
run: | run: |
uv run make ci-test uv run make ci-test
test-unittest: test-unittest:
name: test-unittest - PostgreSQL ${{ matrix.psql }} - Run ${{ matrix.run_id }}/5 name: "Unit tests - PostgreSQL ${{ matrix.psql }} - Run ${{ matrix.run_id }}/5"
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 20 timeout-minutes: 20
needs: test-make-seed needs: test-make-seed
@ -146,6 +149,7 @@ jobs:
file: unittest.xml file: unittest.xml
token: ${{ secrets.CODECOV_TOKEN }} token: ${{ secrets.CODECOV_TOKEN }}
test-integration: test-integration:
name: "Integration tests"
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 30 timeout-minutes: 30
steps: steps:
@ -154,7 +158,7 @@ jobs:
uses: ./.github/actions/setup uses: ./.github/actions/setup
- name: Create k8s Kind Cluster - name: Create k8s Kind Cluster
uses: helm/kind-action@v1.12.0 uses: helm/kind-action@v1.12.0
- name: run integration - name: Run integration
run: | run: |
uv run coverage run manage.py test tests/integration uv run coverage run manage.py test tests/integration
uv run coverage xml uv run coverage xml
@ -170,49 +174,50 @@ jobs:
file: unittest.xml file: unittest.xml
token: ${{ secrets.CODECOV_TOKEN }} token: ${{ secrets.CODECOV_TOKEN }}
test-e2e: test-e2e:
name: test-e2e (${{ matrix.job.name }}) name: "Test E2E (${{ matrix.job.name }})"
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 30 timeout-minutes: 30
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
job: job:
- name: proxy - name: Proxy Provider
glob: tests/e2e/test_provider_proxy* glob: tests/e2e/test_provider_proxy*
- name: oauth - name: OAuth2 Provider
glob: tests/e2e/test_provider_oauth2* tests/e2e/test_source_oauth* glob: tests/e2e/test_provider_oauth2* tests/e2e/test_source_oauth*
- name: oauth-oidc - name: OIDC Provider
glob: tests/e2e/test_provider_oidc* glob: tests/e2e/test_provider_oidc*
- name: saml - name: SAML Provider
glob: tests/e2e/test_provider_saml* tests/e2e/test_source_saml* glob: tests/e2e/test_provider_saml* tests/e2e/test_source_saml*
- name: ldap - name: LDAP Provider
glob: tests/e2e/test_provider_ldap* tests/e2e/test_source_ldap* glob: tests/e2e/test_provider_ldap* tests/e2e/test_source_ldap*
- name: radius - name: RADIUS Provider
glob: tests/e2e/test_provider_radius* glob: tests/e2e/test_provider_radius*
- name: scim - name: SCIM Source
glob: tests/e2e/test_source_scim* glob: tests/e2e/test_source_scim*
- name: flows - name: Flows
glob: tests/e2e/test_flows* glob: tests/e2e/test_flows*
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Setup authentik env - name: Setup authentik env
uses: ./.github/actions/setup uses: ./.github/actions/setup
- name: Setup e2e env (chrome, etc) - name: Setup E2E env (chrome, etc)
run: | run: |
docker compose -f tests/e2e/docker-compose.yml up -d --quiet-pull docker compose -f tests/e2e/docker-compose.yml up -d --quiet-pull
- id: cache-web - id: cache-web
uses: actions/cache@v4 uses: actions/cache@v4
with: with:
path: web/dist path: web/dist
key: ${{ runner.os }}-web-${{ hashFiles('web/package-lock.json', 'web/src/**') }} key: ${{ runner.os }}-web-${{ hashFiles('./package-lock.json', 'web/src/**') }}
- name: prepare web ui - name: Prepare Web UI
if: steps.cache-web.outputs.cache-hit != 'true' if: steps.cache-web.outputs.cache-hit != 'true'
working-directory: web
run: | run: |
npm ci npm ci
make -C .. gen-client-ts make gen-client-ts
npm run build npm run build -w @goauthentik/web
- name: run e2e
npm run typecheck
- name: Run E2E tests
run: | run: |
uv run coverage run manage.py test ${{ matrix.job.glob }} uv run coverage run manage.py test ${{ matrix.job.glob }}
uv run coverage xml uv run coverage xml
@ -228,6 +233,7 @@ jobs:
file: unittest.xml file: unittest.xml
token: ${{ secrets.CODECOV_TOKEN }} token: ${{ secrets.CODECOV_TOKEN }}
ci-core-mark: ci-core-mark:
name: "CI Core Mark"
if: always() if: always()
needs: needs:
- lint - lint
@ -242,6 +248,7 @@ jobs:
with: with:
jobs: ${{ toJSON(needs) }} jobs: ${{ toJSON(needs) }}
build: build:
name: "Build"
permissions: permissions:
# Needed to upload container images to ghcr.io # Needed to upload container images to ghcr.io
packages: write packages: write
@ -255,6 +262,7 @@ jobs:
image_name: ghcr.io/goauthentik/dev-server image_name: ghcr.io/goauthentik/dev-server
release: false release: false
pr-comment: pr-comment:
name: "PR Comment"
needs: needs:
- build - build
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -267,7 +275,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
ref: ${{ github.event.pull_request.head.sha }} ref: ${{ github.event.pull_request.head.sha }}
- name: prepare variables - name: Prepare variables
uses: ./.github/actions/docker-push-variables uses: ./.github/actions/docker-push-variables
id: ev id: ev
env: env:

View File

@ -1,5 +1,5 @@
--- ---
name: authentik-ci-outpost name: "authentik CI Outpost"
on: on:
push: push:
@ -14,6 +14,7 @@ on:
jobs: jobs:
lint-golint: lint-golint:
name: "Lint Go"
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@ -26,7 +27,7 @@ jobs:
mkdir -p web/dist mkdir -p web/dist
mkdir -p website/help mkdir -p website/help
touch web/dist/test website/help/test touch web/dist/test website/help/test
- name: Generate API - name: Generate Go API Client
run: make gen-client-go run: make gen-client-go
- name: golangci-lint - name: golangci-lint
uses: golangci/golangci-lint-action@v7 uses: golangci/golangci-lint-action@v7
@ -35,6 +36,7 @@ jobs:
args: --timeout 5000s --verbose args: --timeout 5000s --verbose
skip-cache: true skip-cache: true
test-unittest: test-unittest:
name: "Unit Test Go"
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@ -43,12 +45,13 @@ jobs:
go-version-file: "go.mod" go-version-file: "go.mod"
- name: Setup authentik env - name: Setup authentik env
uses: ./.github/actions/setup uses: ./.github/actions/setup
- name: Generate API - name: Generate Go API Client
run: make gen-client-go run: make gen-client-go
- name: Go unittests - name: Go unittests
run: | run: |
go test -timeout 0 -v -race -coverprofile=coverage.out -covermode=atomic -cover ./... go test -timeout 0 -v -race -coverprofile=coverage.out -covermode=atomic -cover ./...
ci-outpost-mark: ci-outpost-mark:
name: "CI Outpost Mark"
if: always() if: always()
needs: needs:
- lint-golint - lint-golint
@ -59,6 +62,7 @@ jobs:
with: with:
jobs: ${{ toJSON(needs) }} jobs: ${{ toJSON(needs) }}
build-container: build-container:
name: "Build Container"
timeout-minutes: 120 timeout-minutes: 120
needs: needs:
- ci-outpost-mark - ci-outpost-mark
@ -85,7 +89,7 @@ jobs:
uses: docker/setup-qemu-action@v3.6.0 uses: docker/setup-qemu-action@v3.6.0
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
- name: prepare variables - name: Prepare variables
uses: ./.github/actions/docker-push-variables uses: ./.github/actions/docker-push-variables
id: ev id: ev
env: env:
@ -99,7 +103,7 @@ jobs:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Generate API - name: Generate Go API Client
run: make gen-client-go run: make gen-client-go
- name: Build Docker Image - name: Build Docker Image
id: push id: push
@ -122,6 +126,7 @@ jobs:
subject-digest: ${{ steps.push.outputs.digest }} subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: true push-to-registry: true
build-binary: build-binary:
name: "Build Binary"
timeout-minutes: 120 timeout-minutes: 120
needs: needs:
- ci-outpost-mark - ci-outpost-mark
@ -140,21 +145,22 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
ref: ${{ github.event.pull_request.head.sha }} ref: ${{ github.event.pull_request.head.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: package.json
cache: "npm"
cache-dependency-path: package-lock.json
- name: Install Node.js dependencies
run: npm ci
- uses: actions/setup-go@v5 - uses: actions/setup-go@v5
with: with:
go-version-file: "go.mod" go-version-file: "go.mod"
- uses: actions/setup-node@v4 - name: Generate Go API Client
with:
node-version-file: web/package.json
cache: "npm"
cache-dependency-path: web/package-lock.json
- name: Generate API
run: make gen-client-go run: make gen-client-go
- name: Build web - name: Build web
working-directory: web/
run: | run: |
npm ci npm ci
npm run build-proxy npm run build-proxy -w @goauthentik/web
- name: Build outpost - name: Build outpost
run: | run: |
set -x set -x

View File

@ -1,4 +1,4 @@
name: authentik-ci-web name: CI Web UI
on: on:
push: push:
@ -13,54 +13,50 @@ on:
jobs: jobs:
lint: lint:
runs-on: ubuntu-latest name: Lint
strategy:
fail-fast: false
matrix:
command:
- lint
- lint:lockfile
- tsc
- prettier-check
project:
- web
include:
- command: tsc
project: web
- command: lit-analyse
project: web
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: ${{ matrix.project }}/package.json
cache: "npm"
cache-dependency-path: ${{ matrix.project }}/package-lock.json
- working-directory: ${{ matrix.project }}/
run: |
npm ci
- name: Generate API
run: make gen-client-ts
- name: Lint
working-directory: ${{ matrix.project }}/
run: npm run ${{ matrix.command }}
build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version-file: web/package.json node-version-file: package.json
cache: "npm" cache: "npm"
cache-dependency-path: web/package-lock.json cache-dependency-path: package-lock.json
- working-directory: web/ - name: Install Node.js dependencies
run: npm ci run: npm ci
- name: Generate API - name: Generate TypeScript API
run: make gen-client-ts
- name: Build
run: |
npm run build -w @goauthentik/web
- name: Type check
run: |
npm run typecheck
- name: Lint
run: |
npm run lint -w @goauthentik/web
npm run lint:lockfile -w @goauthentik/web
npm run lit-analyse -w @goauthentik/web
build:
name: Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: package.json
cache: "npm"
cache-dependency-path: package-lock.json
- name: Install Node.js dependencies
run: npm ci
- name: Generate TypeScript API
run: make gen-client-ts run: make gen-client-ts
- name: build - name: build
working-directory: web/ run: |
run: npm run build npm run build -w @goauthentik/web
npm run typecheck
ci-web-mark: ci-web-mark:
name: CI Web Mark
if: always() if: always()
needs: needs:
- build - build
@ -71,6 +67,7 @@ jobs:
with: with:
jobs: ${{ toJSON(needs) }} jobs: ${{ toJSON(needs) }}
test: test:
name: Test
needs: needs:
- ci-web-mark - ci-web-mark
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -78,13 +75,12 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version-file: web/package.json node-version-file: package.json
cache: "npm" cache: "npm"
cache-dependency-path: web/package-lock.json cache-dependency-path: package-lock.json
- working-directory: web/ - name: Install Node.js dependencies
run: npm ci run: npm ci
- name: Generate API - name: Generate TypeScript API
run: make gen-client-ts run: make gen-client-ts
- name: test - name: Test Web UI
working-directory: web/ run: npm run test -w @goauthentik/web || exit 0
run: npm run test || exit 0

View File

@ -1,4 +1,4 @@
name: authentik-ci-website name: CI Docs Website
on: on:
push: push:
@ -13,55 +13,59 @@ on:
jobs: jobs:
lint: lint:
name: "Lint"
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
command:
- lint:lockfile
- prettier-check
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- working-directory: website/ - uses: actions/setup-node@v4
run: npm ci with:
- name: Lint node-version-file: package.json
working-directory: website/ cache: "npm"
run: npm run ${{ matrix.command }} cache-dependency-path: package-lock.json
- name: Install Node.js dependencies
run: |
npm ci
- name: Generate TypeScript API
run: make gen-client-ts
- name: Lint Docs
run: |
npm run lint:prettier:check
npm run lint:lockfile -w @goauthentik/docs
test: test:
name: "Test Docs"
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version-file: website/package.json node-version-file: package.json
cache: "npm" cache: "npm"
cache-dependency-path: website/package-lock.json cache-dependency-path: package-lock.json
- working-directory: website/ - name: Install Node.js dependencies
run: npm ci run: |
- name: test npm ci
working-directory: website/ - name: Generate TypeScript API
run: npm test run: make gen-client-ts
- name: Test Docs
run: |
npm run test -w @goauthentik/docs
build: build:
name: "Build Docs"
runs-on: ubuntu-latest runs-on: ubuntu-latest
name: ${{ matrix.job }}
strategy:
fail-fast: false
matrix:
job:
- build
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version-file: website/package.json node-version-file: package.json
cache: "npm" cache: "npm"
cache-dependency-path: website/package-lock.json cache-dependency-path: package-lock.json
- working-directory: website/ - name: Install Node.js dependencies
run: npm ci run: npm ci
- name: build - name: Build
working-directory: website/ run: |
run: npm run ${{ matrix.job }} npm run build -w @goauthentik/docs
ci-website-mark: ci-website-mark:
name: "CI Website Mark"
if: always() if: always()
needs: needs:
- lint - lint

View File

@ -10,7 +10,7 @@ on:
jobs: jobs:
analyze: analyze:
name: Analyze name: "Analyze"
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
actions: read actions: read

View File

@ -1,4 +1,4 @@
name: authentik-gen-update-webauthn-mds name: "authentik CI Update WebAuthn MDS"
on: on:
workflow_dispatch: workflow_dispatch:
schedule: schedule:
@ -11,6 +11,7 @@ env:
jobs: jobs:
build: build:
name: "Update WebAuthn MDS"
if: ${{ github.repository != 'goauthentik/authentik-internal' }} if: ${{ github.repository != 'goauthentik/authentik-internal' }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:

View File

@ -1,6 +1,6 @@
--- ---
# See https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#force-deleting-cache-entries # See https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#force-deleting-cache-entries
name: Cleanup cache after PR is closed name: "Post-PR Closed Cache Cleanup"
on: on:
pull_request: pull_request:
types: types:
@ -12,6 +12,7 @@ permissions:
jobs: jobs:
cleanup: cleanup:
name: "Cleanup Cache"
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check out code - name: Check out code

View File

@ -1,4 +1,4 @@
name: ghcr-retention name: "authentik GHCR Retention Policy"
on: on:
# schedule: # schedule:
@ -8,7 +8,7 @@ on:
jobs: jobs:
clean-ghcr: clean-ghcr:
if: ${{ github.repository != 'goauthentik/authentik-internal' }} if: ${{ github.repository != 'goauthentik/authentik-internal' }}
name: Delete old unused container images name: "Delete old unused container images"
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- id: generate_token - id: generate_token

View File

@ -1,5 +1,5 @@
--- ---
name: authentik-compress-images name: "authentik CI Image Compression"
on: on:
push: push:
@ -20,7 +20,7 @@ on:
jobs: jobs:
compress: compress:
name: compress name: "Compress Docker images"
runs-on: ubuntu-latest runs-on: ubuntu-latest
# Don't run on forks. Token will not be available. Will run on main and open a PR anyway # Don't run on forks. Token will not be available. Will run on main and open a PR anyway
if: | if: |

View File

@ -1,4 +1,4 @@
name: authentik-publish-source-docs name: "authentik Publish Source Docs"
on: on:
push: push:
@ -12,6 +12,7 @@ env:
jobs: jobs:
publish-source-docs: publish-source-docs:
name: "Publish"
if: ${{ github.repository != 'goauthentik/authentik-internal' }} if: ${{ github.repository != 'goauthentik/authentik-internal' }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 120 timeout-minutes: 120
@ -19,11 +20,11 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Setup authentik env - name: Setup authentik env
uses: ./.github/actions/setup uses: ./.github/actions/setup
- name: generate docs - name: Generate docs
run: | run: |
uv run make migrate uv run make migrate
uv run ak build_source_docs uv run ak build_source_docs
- name: Publish - name: Deploy to Netlify
uses: netlify/actions/cli@master uses: netlify/actions/cli@master
with: with:
args: deploy --dir=source_docs --prod args: deploy --dir=source_docs --prod

View File

@ -1,4 +1,4 @@
name: authentik-on-release-next-branch name: "authentik on Release Next Branch"
on: on:
schedule: schedule:
@ -11,6 +11,7 @@ permissions:
jobs: jobs:
update-next: update-next:
name: "Update Next Branch"
if: ${{ github.repository != 'goauthentik/authentik-internal' }} if: ${{ github.repository != 'goauthentik/authentik-internal' }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
environment: internal-production environment: internal-production

View File

@ -1,5 +1,5 @@
--- ---
name: authentik-on-release name: "Release publish"
on: on:
release: release:
@ -7,6 +7,7 @@ on:
jobs: jobs:
build-server: build-server:
name: "Build server"
uses: ./.github/workflows/_reusable-docker-build.yaml uses: ./.github/workflows/_reusable-docker-build.yaml
secrets: inherit secrets: inherit
permissions: permissions:
@ -21,6 +22,7 @@ jobs:
registry_dockerhub: true registry_dockerhub: true
registry_ghcr: true registry_ghcr: true
build-outpost: build-outpost:
name: "Build outpost"
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
# Needed to upload container images to ghcr.io # Needed to upload container images to ghcr.io
@ -45,14 +47,14 @@ jobs:
uses: docker/setup-qemu-action@v3.6.0 uses: docker/setup-qemu-action@v3.6.0
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
- name: prepare variables - name: Prepare variables
uses: ./.github/actions/docker-push-variables uses: ./.github/actions/docker-push-variables
id: ev id: ev
env: env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
with: with:
image-name: ghcr.io/goauthentik/${{ matrix.type }},beryju/authentik-${{ matrix.type }} image-name: ghcr.io/goauthentik/${{ matrix.type }},beryju/authentik-${{ matrix.type }}
- name: make empty clients - name: Make empty clients
run: | run: |
mkdir -p ./gen-ts-api mkdir -p ./gen-ts-api
mkdir -p ./gen-go-api mkdir -p ./gen-go-api
@ -85,6 +87,7 @@ jobs:
subject-digest: ${{ steps.push.outputs.digest }} subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: true push-to-registry: true
build-outpost-binary: build-outpost-binary:
name: "Build outpost binary"
timeout-minutes: 120 timeout-minutes: 120
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
@ -106,14 +109,13 @@ jobs:
go-version-file: "go.mod" go-version-file: "go.mod"
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version-file: web/package.json node-version-file: package.json
cache: "npm" cache: "npm"
cache-dependency-path: web/package-lock.json cache-dependency-path: package-lock.json
- name: Build web - name: Build web
working-directory: web/
run: | run: |
npm ci npm ci
npm run build-proxy npm run build-proxy -w @goauthentik/web
- name: Build outpost - name: Build outpost
run: | run: |
set -x set -x
@ -129,6 +131,7 @@ jobs:
asset_name: authentik-outpost-${{ matrix.type }}_${{ matrix.goos }}_${{ matrix.goarch }} asset_name: authentik-outpost-${{ matrix.type }}_${{ matrix.goos }}_${{ matrix.goarch }}
tag: ${{ github.ref }} tag: ${{ github.ref }}
upload-aws-cfn-template: upload-aws-cfn-template:
name: "Upload AWS CloudFormation template"
permissions: permissions:
# Needed for AWS login # Needed for AWS login
id-token: write id-token: write
@ -150,6 +153,7 @@ jobs:
aws s3 cp --acl=public-read lifecycle/aws/template.yaml s3://authentik-cloudformation-templates/authentik.ecs.${{ github.ref }}.yaml aws s3 cp --acl=public-read lifecycle/aws/template.yaml s3://authentik-cloudformation-templates/authentik.ecs.${{ github.ref }}.yaml
aws s3 cp --acl=public-read lifecycle/aws/template.yaml s3://authentik-cloudformation-templates/authentik.ecs.latest.yaml aws s3 cp --acl=public-read lifecycle/aws/template.yaml s3://authentik-cloudformation-templates/authentik.ecs.latest.yaml
test-release: test-release:
name: "Test release"
needs: needs:
- build-server - build-server
- build-outpost - build-outpost
@ -166,6 +170,7 @@ jobs:
docker compose start postgresql redis docker compose start postgresql redis
docker compose run -u root server test-all docker compose run -u root server test-all
sentry-release: sentry-release:
name: "Sentry release"
needs: needs:
- build-server - build-server
- build-outpost - build-outpost
@ -173,7 +178,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: prepare variables - name: Prepare variables
uses: ./.github/actions/docker-push-variables uses: ./.github/actions/docker-push-variables
id: ev id: ev
env: env:

View File

@ -1,5 +1,5 @@
--- ---
name: authentik-on-tag name: "authentik on Tag Release"
on: on:
push: push:
@ -8,7 +8,7 @@ on:
jobs: jobs:
build: build:
name: Create Release from Tag name: "Create Release from Tag"
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@ -20,7 +20,7 @@ jobs:
with: with:
app_id: ${{ secrets.GH_APP_ID }} app_id: ${{ secrets.GH_APP_ID }}
private_key: ${{ secrets.GH_APP_PRIVATE_KEY }} private_key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- name: prepare variables - name: Prepare variables
uses: ./.github/actions/docker-push-variables uses: ./.github/actions/docker-push-variables
id: ev id: ev
env: env:

View File

@ -1,13 +1,15 @@
name: "authentik-repo-mirror" name: "authentik Repository Mirror"
on: [push, delete] on: [push, delete]
jobs: jobs:
to_internal: to_internal:
name: "Mirror to internal repository"
if: ${{ github.repository != 'goauthentik/authentik-internal' }} if: ${{ github.repository != 'goauthentik/authentik-internal' }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
name: "Checkout repository"
with: with:
fetch-depth: 0 fetch-depth: 0
- if: ${{ env.MIRROR_KEY != '' }} - if: ${{ env.MIRROR_KEY != '' }}

View File

@ -1,4 +1,4 @@
name: "authentik-repo-stale" name: "authentik Repository Stale Issues"
on: on:
schedule: schedule:
@ -11,6 +11,7 @@ permissions:
jobs: jobs:
stale: stale:
name: "Stale Issues"
if: ${{ github.repository != 'goauthentik/authentik-internal' }} if: ${{ github.repository != 'goauthentik/authentik-internal' }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:

View File

@ -1,4 +1,4 @@
name: authentik-semgrep name: "authentik CI Semgrep"
on: on:
workflow_dispatch: {} workflow_dispatch: {}
pull_request: {} pull_request: {}
@ -13,7 +13,7 @@ on:
- cron: '12 15 * * *' - cron: '12 15 * * *'
jobs: jobs:
semgrep: semgrep:
name: semgrep/ci name: "semgrep/ci"
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
contents: read contents: read

View File

@ -1,4 +1,4 @@
name: authentik-translation-advice name: "authentik Translations Advice"
on: on:
pull_request: pull_request:
@ -16,6 +16,7 @@ permissions:
jobs: jobs:
post-comment: post-comment:
name: "Post Comment"
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Find Comment - name: Find Comment

View File

@ -1,5 +1,5 @@
--- ---
name: authentik-translate-extract-compile name: "authentik Extract & Compile Translations"
on: on:
schedule: schedule:
- cron: "0 0 * * *" # every day at midnight - cron: "0 0 * * *" # every day at midnight
@ -16,6 +16,7 @@ env:
jobs: jobs:
compile: compile:
name: "Compile Translations"
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- id: generate_token - id: generate_token
@ -32,15 +33,20 @@ jobs:
if: ${{ github.event_name == 'pull_request' }} if: ${{ github.event_name == 'pull_request' }}
- name: Setup authentik env - name: Setup authentik env
uses: ./.github/actions/setup uses: ./.github/actions/setup
- name: Generate API - name: Generate TypeScript API
run: make gen-client-ts run: make gen-client-ts
- name: run extract - name: Extract Translations
run: | run: |
uv run make i18n-extract uv run make i18n-extract
- name: run compile - name: Build Docs Site
run: npm run build-bundled -w @goauthentik/docs
- name: Build Web UI
run: npm run build -w @goauthentik/web
- name: Type check
run: npm run typecheck
- name: Compile Messages
run: | run: |
uv run ak compilemessages uv run ak compilemessages
make web-check-compile
- name: Create Pull Request - name: Create Pull Request
if: ${{ github.event_name != 'pull_request' }} if: ${{ github.event_name != 'pull_request' }}
uses: peter-evans/create-pull-request@v7 uses: peter-evans/create-pull-request@v7

View File

@ -1,6 +1,6 @@
# Rename transifex pull requests to have a correct naming # Rename transifex pull requests to have a correct naming
# Also enables auto squash-merge # Also enables auto squash-merge
name: authentik-translation-transifex-rename name: "authentik Translations Transifex PR Rename"
on: on:
pull_request: pull_request:
@ -12,6 +12,7 @@ permissions:
jobs: jobs:
rename_pr: rename_pr:
name: "Rename PR"
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: ${{ github.event.pull_request.user.login == 'transifex-integration[bot]'}} if: ${{ github.event.pull_request.user.login == 'transifex-integration[bot]'}}
steps: steps:

23
.gitignore vendored
View File

@ -217,3 +217,26 @@ source_docs/
### Docker ### ### Docker ###
docker-compose.override.yml docker-compose.override.yml
### Node ###
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
node_modules/
tsconfig.tsbuildinfo
# Wireit's cache
.wireit
custom-elements.json
### Development ###
.drafts

View File

@ -4,12 +4,16 @@
**/LICENSE **/LICENSE
authentik/stages/**/* authentik/stages/**/*
authentik/sources/**/*
schemas/**/*
blueprints/**/*
## Build asset directories ## Build asset directories
coverage coverage
dist dist
out out
.docusaurus .docusaurus
.wireit
website/docs/developer-docs/api/**/* website/docs/developer-docs/api/**/*
## Environment ## Environment
@ -32,14 +36,15 @@ coverage
# Templates # Templates
# TODO: Rename affected files to *.template.* or similar. # TODO: Rename affected files to *.template.* or similar.
authentik/**/*.html
*.html *.html
*.mdx *.mdx
*.md *.md
## Import order matters ## Import order matters
poly.ts web/src/poly.ts
src/locale-codes.ts web/src/locale-codes.ts
src/locales/ web/src/locales/
# Storybook # Storybook
storybook-static/ storybook-static/

View File

@ -17,6 +17,6 @@
"ms-python.vscode-pylance", "ms-python.vscode-pylance",
"redhat.vscode-yaml", "redhat.vscode-yaml",
"Tobermory.es6-string-html", "Tobermory.es6-string-html",
"unifiedjs.vscode-mdx", "unifiedjs.vscode-mdx"
] ]
} }

72
.vscode/settings.json vendored
View File

@ -16,7 +16,7 @@
], ],
"typescript.preferences.importModuleSpecifier": "non-relative", "typescript.preferences.importModuleSpecifier": "non-relative",
"typescript.preferences.importModuleSpecifierEnding": "index", "typescript.preferences.importModuleSpecifierEnding": "index",
"typescript.tsdk": "./web/node_modules/typescript/lib", "typescript.tsdk": "./node_modules/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true, "typescript.enablePromptUseWorkspaceTsdk": true,
"yaml.schemas": { "yaml.schemas": {
"./blueprints/schema.json": "blueprints/**/*.yaml" "./blueprints/schema.json": "blueprints/**/*.yaml"
@ -30,7 +30,71 @@
} }
], ],
"go.testFlags": ["-count=1"], "go.testFlags": ["-count=1"],
"github-actions.workflows.pinned.workflows": [ "github-actions.workflows.pinned.workflows": [".github/workflows/ci-main.yml"],
".github/workflows/ci-main.yml"
] "eslint.useFlatConfig": true,
"explorer.fileNesting.enabled": true,
"explorer.fileNesting.patterns": {
"*.mjs": "*.d.mts",
"*.cjs": "*.d.cts",
"package.json": "package-lock.json, yarn.lock, .yarnrc, .yarnrc.yml, .yarn, .nvmrc, .node-version",
"tsconfig.json": "tsconfig.*.json, jsconfig.json",
"Dockerfile": "*.Dockerfile"
},
"search.exclude": {
"**/node_modules": true,
"**/*.code-search": true,
"**/dist": true,
"**/out": true,
"**/package-lock.json": true
},
"[css]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[markdown]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[shellscript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[django-html]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"editor.codeActionsOnSave": {
"source.removeUnusedImports": "explicit"
},
// We use Prettier for formatting, but specifying these settings
// will ensure that VS Code's IntelliSense doesn't autocomplete unformatted code.
"javascript.format.semicolons": "insert",
"typescript.format.semicolons": "insert",
"javascript.preferences.quoteStyle": "double",
"typescript.preferences.quoteStyle": "double",
"github.copilot.enable": {
"*": true,
"plaintext": true,
"markdown": true,
"scminput": false,
"csv": false,
"json": true,
"yaml": true
}
} }

40
.vscode/tasks.json vendored
View File

@ -4,12 +4,7 @@
{ {
"label": "authentik/core: make", "label": "authentik/core: make",
"command": "uv", "command": "uv",
"args": [ "args": ["run", "make", "lint-fix", "lint"],
"run",
"make",
"lint-fix",
"lint"
],
"presentation": { "presentation": {
"panel": "new" "panel": "new"
}, },
@ -18,11 +13,7 @@
{ {
"label": "authentik/core: run", "label": "authentik/core: run",
"command": "uv", "command": "uv",
"args": [ "args": ["run", "ak", "server"],
"run",
"ak",
"server"
],
"group": "build", "group": "build",
"presentation": { "presentation": {
"panel": "dedicated", "panel": "dedicated",
@ -32,17 +23,13 @@
{ {
"label": "authentik/web: make", "label": "authentik/web: make",
"command": "make", "command": "make",
"args": [ "args": ["web"],
"web"
],
"group": "build" "group": "build"
}, },
{ {
"label": "authentik/web: watch", "label": "authentik/web: watch",
"command": "make", "command": "make",
"args": [ "args": ["web-watch"],
"web-watch"
],
"group": "build", "group": "build",
"presentation": { "presentation": {
"panel": "dedicated", "panel": "dedicated",
@ -52,26 +39,19 @@
{ {
"label": "authentik: install", "label": "authentik: install",
"command": "make", "command": "make",
"args": [ "args": ["install", "-j4"],
"install",
"-j4"
],
"group": "build" "group": "build"
}, },
{ {
"label": "authentik/website: make", "label": "authentik/website: make",
"command": "make", "command": "make",
"args": [ "args": ["website"],
"website"
],
"group": "build" "group": "build"
}, },
{ {
"label": "authentik/website: watch", "label": "authentik/website: watch",
"command": "make", "command": "make",
"args": [ "args": ["website-watch"],
"website-watch"
],
"group": "build", "group": "build",
"presentation": { "presentation": {
"panel": "dedicated", "panel": "dedicated",
@ -81,11 +61,7 @@
{ {
"label": "authentik/api: generate", "label": "authentik/api: generate",
"command": "uv", "command": "uv",
"args": [ "args": ["run", "make", "gen"],
"run",
"make",
"gen"
],
"group": "build" "group": "build"
} }
] ]

View File

@ -1,48 +1,31 @@
# syntax=docker/dockerfile:1 # syntax=docker/dockerfile:1
# Stage 1: Build website # Stage 1 Web UI and Documentation build
FROM --platform=${BUILDPLATFORM} docker.io/library/node:22 AS website-builder
ENV NODE_ENV=production
WORKDIR /work/website
RUN --mount=type=bind,target=/work/website/package.json,src=./website/package.json \
--mount=type=bind,target=/work/website/package-lock.json,src=./website/package-lock.json \
--mount=type=cache,id=npm-website,sharing=shared,target=/root/.npm \
npm ci --include=dev
COPY ./website /work/website/
COPY ./blueprints /work/blueprints/
COPY ./schema.yml /work/
COPY ./SECURITY.md /work/
RUN npm run build-bundled
# Stage 2: Build webui
FROM --platform=${BUILDPLATFORM} docker.io/library/node:22 AS web-builder FROM --platform=${BUILDPLATFORM} docker.io/library/node:22 AS web-builder
ARG GIT_BUILD_HASH
ENV GIT_BUILD_HASH=$GIT_BUILD_HASH
ENV NODE_ENV=production ENV NODE_ENV=production
WORKDIR /work/web WORKDIR /work
RUN --mount=type=bind,target=/work/web/package.json,src=./web/package.json \ COPY ./package.json ./package.json
--mount=type=bind,target=/work/web/package-lock.json,src=./web/package-lock.json \ COPY ./package-lock.json ./package-lock.json
--mount=type=bind,target=/work/web/packages/sfe/package.json,src=./web/packages/sfe/package.json \ COPY ./packages ./packages
--mount=type=bind,target=/work/web/scripts,src=./web/scripts \ COPY ./web ./web
--mount=type=cache,id=npm-web,sharing=shared,target=/root/.npm \ COPY ./website ./website
npm ci --include=dev
COPY ./package.json /work COPY ./gen-ts-api ./gen-ts-api
COPY ./web /work/web/ COPY ./blueprints ./blueprints
COPY ./website /work/website/ COPY ./schema.yml ./schema.yml
COPY ./gen-ts-api /work/web/node_modules/@goauthentik/api COPY ./SECURITY.md ./SECURITY.md
RUN npm run build RUN --mount=type=cache,target=/root/.npm npm ci --include=dev
RUN npm run build-bundled -w @goauthentik/docs
RUN npm run build -w @goauthentik/web
# Stage 2: Build go proxy
# Stage 3: Build go proxy
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.24-bookworm AS go-builder FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.24-bookworm AS go-builder
ARG TARGETOS ARG TARGETOS
@ -79,7 +62,8 @@ RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \
CGO_ENABLED=1 GOFIPS140=latest GOARM="${TARGETVARIANT#v}" \ CGO_ENABLED=1 GOFIPS140=latest GOARM="${TARGETVARIANT#v}" \
go build -o /go/authentik ./cmd/server go build -o /go/authentik ./cmd/server
# Stage 4: MaxMind GeoIP # Stage 3: MaxMind GeoIP
FROM --platform=${BUILDPLATFORM} ghcr.io/maxmind/geoipupdate:v7.1.0 AS geoip FROM --platform=${BUILDPLATFORM} ghcr.io/maxmind/geoipupdate:v7.1.0 AS geoip
ENV GEOIPUPDATE_EDITION_IDS="GeoLite2-City GeoLite2-ASN" ENV GEOIPUPDATE_EDITION_IDS="GeoLite2-City GeoLite2-ASN"
@ -93,9 +77,10 @@ RUN --mount=type=secret,id=GEOIPUPDATE_ACCOUNT_ID \
mkdir -p /usr/share/GeoIP && \ mkdir -p /usr/share/GeoIP && \
/bin/sh -c "/usr/bin/entry.sh || echo 'Failed to get GeoIP database, disabling'; exit 0" /bin/sh -c "/usr/bin/entry.sh || echo 'Failed to get GeoIP database, disabling'; exit 0"
# Stage 5: Download uv # Stage 4: Download uv
FROM ghcr.io/astral-sh/uv:0.6.14 AS uv FROM ghcr.io/astral-sh/uv:0.6.14 AS uv
# Stage 6: Base python image
# Stage 5: Base python image
FROM ghcr.io/goauthentik/fips-python:3.12.10-slim-bookworm-fips AS python-base FROM ghcr.io/goauthentik/fips-python:3.12.10-slim-bookworm-fips AS python-base
ENV VENV_PATH="/ak-root/.venv" \ ENV VENV_PATH="/ak-root/.venv" \
@ -109,7 +94,7 @@ WORKDIR /ak-root/
COPY --from=uv /uv /uvx /bin/ COPY --from=uv /uv /uvx /bin/
# Stage 7: Python dependencies # Stage 6: Python dependencies
FROM python-base AS python-deps FROM python-base AS python-deps
ARG TARGETARCH ARG TARGETARCH
@ -144,7 +129,7 @@ RUN --mount=type=bind,target=pyproject.toml,src=pyproject.toml \
--mount=type=cache,target=/root/.cache/uv \ --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen --no-install-project --no-dev uv sync --frozen --no-install-project --no-dev
# Stage 8: Run # Stage 7: Run
FROM python-base AS final-image FROM python-base AS final-image
ARG VERSION ARG VERSION
@ -189,7 +174,7 @@ COPY --from=go-builder /go/authentik /bin/authentik
COPY --from=python-deps /ak-root/.venv /ak-root/.venv COPY --from=python-deps /ak-root/.venv /ak-root/.venv
COPY --from=web-builder /work/web/dist/ /web/dist/ COPY --from=web-builder /work/web/dist/ /web/dist/
COPY --from=web-builder /work/web/authentik/ /web/authentik/ COPY --from=web-builder /work/web/authentik/ /web/authentik/
COPY --from=website-builder /work/website/build/ /website/help/ COPY --from=web-builder /work/website/build/ /website/help/
COPY --from=geoip /usr/share/GeoIP /geoip COPY --from=geoip /usr/share/GeoIP /geoip
USER 1000 USER 1000

107
Makefile
View File

@ -36,6 +36,13 @@ test: ## Run the server tests and produce a coverage report (locally)
uv run coverage html uv run coverage html
uv run coverage report uv run coverage report
node-check-compile: ## Check and compile the TypeScript source code
npm run typecheck
node-lint-fix: ## Lint and automatically fix errors in the javascript source code
lint-codespell
npm run lint:fix
lint-fix: lint-codespell ## Lint and automatically fix errors in the python source code. Reports spelling errors. lint-fix: lint-codespell ## Lint and automatically fix errors in the python source code. Reports spelling errors.
uv run black $(PY_SOURCES) uv run black $(PY_SOURCES)
uv run ruff check --fix $(PY_SOURCES) uv run ruff check --fix $(PY_SOURCES)
@ -47,9 +54,6 @@ lint: ## Lint the python and golang sources
uv run bandit -c pyproject.toml -r $(PY_SOURCES) uv run bandit -c pyproject.toml -r $(PY_SOURCES)
golangci-lint run -v golangci-lint run -v
core-install:
uv sync --frozen
migrate: ## Run the Authentik Django server's migrations migrate: ## Run the Authentik Django server's migrations
uv run python -m lifecycle.migrate uv run python -m lifecycle.migrate
@ -72,7 +76,9 @@ core-i18n-extract:
--ignore website \ --ignore website \
-l en -l en
install: web-install website-install core-install ## Install all requires dependencies for `web`, `website` and `core` install: ## Install all requires dependencies for `web`, `website` and `core`
npm ci
uv sync --frozen
dev-drop-db: dev-drop-db:
dropdb -U ${pg_user} -h ${pg_host} ${pg_name} dropdb -U ${pg_user} -h ${pg_host} ${pg_name}
@ -94,6 +100,7 @@ gen-build: ## Extract the schema from the database
AUTHENTIK_TENANTS__ENABLED=true \ AUTHENTIK_TENANTS__ENABLED=true \
AUTHENTIK_OUTPOSTS__DISABLE_EMBEDDED_OUTPOST=true \ AUTHENTIK_OUTPOSTS__DISABLE_EMBEDDED_OUTPOST=true \
uv run ak make_blueprint_schema > blueprints/schema.json uv run ak make_blueprint_schema > blueprints/schema.json
AUTHENTIK_DEBUG=true \ AUTHENTIK_DEBUG=true \
AUTHENTIK_TENANTS__ENABLED=true \ AUTHENTIK_TENANTS__ENABLED=true \
AUTHENTIK_OUTPOSTS__DISABLE_EMBEDDED_OUTPOST=true \ AUTHENTIK_OUTPOSTS__DISABLE_EMBEDDED_OUTPOST=true \
@ -101,19 +108,24 @@ gen-build: ## Extract the schema from the database
gen-changelog: ## (Release) generate the changelog based from the commits since the last tag gen-changelog: ## (Release) generate the changelog based from the commits since the last tag
git log --pretty=format:" - %s" $(shell git describe --tags $(shell git rev-list --tags --max-count=1))...$(shell git branch --show-current) | sort > changelog.md git log --pretty=format:" - %s" $(shell git describe --tags $(shell git rev-list --tags --max-count=1))...$(shell git branch --show-current) | sort > changelog.md
npx prettier --write changelog.md npx prettier --write changelog.md
gen-diff: ## (Release) generate the changelog diff between the current schema and the last tag gen-diff: ## (Release) generate the changelog diff between the current schema and the last tag
git show $(shell git describe --tags $(shell git rev-list --tags --max-count=1)):schema.yml > old_schema.yml git show $(shell git describe --tags $(shell git rev-list --tags --max-count=1)):schema.yml > old_schema.yml
docker run \ docker run \
--rm -v ${PWD}:/local \ --rm -v ${PWD}:/local \
--user ${UID}:${GID} \ --user ${UID}:${GID} \
docker.io/openapitools/openapi-diff:2.1.0-beta.8 \ docker.io/openapitools/openapi-diff:2.1.0-beta.8 \
--markdown /local/diff.md \ --markdown /local/diff.md \
/local/old_schema.yml /local/schema.yml /local/old_schema.yml /local/schema.yml
rm old_schema.yml rm old_schema.yml
sed -i 's/{/&#123;/g' diff.md sed -i 's/{/&#123;/g' diff.md
sed -i 's/}/&#125;/g' diff.md sed -i 's/}/&#125;/g' diff.md
npx prettier --write diff.md npx prettier --write diff.md
gen-clean-ts: ## Remove generated API client for Typescript gen-clean-ts: ## Remove generated API client for Typescript
@ -133,46 +145,57 @@ gen-client-ts: gen-clean-ts ## Build and install the authentik API for Typescri
--rm -v ${PWD}:/local \ --rm -v ${PWD}:/local \
--user ${UID}:${GID} \ --user ${UID}:${GID} \
docker.io/openapitools/openapi-generator-cli:v7.11.0 generate \ docker.io/openapitools/openapi-generator-cli:v7.11.0 generate \
-i /local/schema.yml \ --input-spec /local/schema.yml \
-g typescript-fetch \ --generator-name typescript-fetch \
-o /local/${GEN_API_TS} \ --output /local/${GEN_API_TS} \
-c /local/scripts/api-ts-config.yaml \ --config /local/scripts/api-ts-config.yaml \
--additional-properties=npmVersion=${NPM_VERSION} \ --additional-properties=npmVersion=${NPM_VERSION} \
--git-repo-id authentik \ --git-repo-id authentik \
--git-user-id goauthentik --git-user-id goauthentik
mkdir -p web/node_modules/@goauthentik/api
cd ./${GEN_API_TS} && npm i npm install
\cp -rf ./${GEN_API_TS}/* web/node_modules/@goauthentik/api
gen-client-py: gen-clean-py ## Build and install the authentik API for Python gen-client-py: gen-clean-py ## Build and install the authentik API for Python
docker run \ docker run \
--rm -v ${PWD}:/local \ --rm -v ${PWD}:/local \
--user ${UID}:${GID} \ --user ${UID}:${GID} \
docker.io/openapitools/openapi-generator-cli:v7.11.0 generate \ docker.io/openapitools/openapi-generator-cli:v7.11.0 generate \
-i /local/schema.yml \ --input-spec /local/schema.yml \
-g python \ --generator-name python \
-o /local/${GEN_API_PY} \ --output /local/${GEN_API_PY} \
-c /local/scripts/api-py-config.yaml \ --config /local/scripts/api-py-config.yaml \
--additional-properties=packageVersion=${NPM_VERSION} \ --additional-properties=packageVersion=${NPM_VERSION} \
--git-repo-id authentik \ --git-repo-id authentik \
--git-user-id goauthentik --git-user-id goauthentik
pip install ./${GEN_API_PY} pip install ./${GEN_API_PY}
gen-client-go: gen-clean-go ## Build and install the authentik API for Golang gen-client-go: gen-clean-go ## Build and install the authentik API for Golang
mkdir -p ./${GEN_API_GO} ./${GEN_API_GO}/templates mkdir -p ./${GEN_API_GO} ./${GEN_API_GO}/templates
wget https://raw.githubusercontent.com/goauthentik/client-go/main/config.yaml -O ./${GEN_API_GO}/config.yaml
wget https://raw.githubusercontent.com/goauthentik/client-go/main/templates/README.mustache -O ./${GEN_API_GO}/templates/README.mustache wget https://raw.githubusercontent.com/goauthentik/client-go/main/config.yaml \
wget https://raw.githubusercontent.com/goauthentik/client-go/main/templates/go.mod.mustache -O ./${GEN_API_GO}/templates/go.mod.mustache -O ./${GEN_API_GO}/config.yaml
wget https://raw.githubusercontent.com/goauthentik/client-go/main/templates/README.mustache \
-O ./${GEN_API_GO}/templates/README.mustache
wget https://raw.githubusercontent.com/goauthentik/client-go/main/templates/go.mod.mustache \
-O ./${GEN_API_GO}/templates/go.mod.mustache
cp schema.yml ./${GEN_API_GO}/ cp schema.yml ./${GEN_API_GO}/
docker run \ docker run \
--rm -v ${PWD}/${GEN_API_GO}:/local \ --rm -v ${PWD}/${GEN_API_GO}:/local \
--user ${UID}:${GID} \ --user ${UID}:${GID} \
docker.io/openapitools/openapi-generator-cli:v6.5.0 generate \ docker.io/openapitools/openapi-generator-cli:v6.5.0 generate \
-i /local/schema.yml \ --input-spec /local/schema.yml \
-g go \ --generator-name go \
-o /local/ \ --output /local/ \
-c /local/config.yaml --config /local/config.yaml
go mod edit -replace goauthentik.io/api/v3=./${GEN_API_GO} go mod edit -replace goauthentik.io/api/v3=./${GEN_API_GO}
rm -rf ./${GEN_API_GO}/config.yaml ./${GEN_API_GO}/templates/ rm -rf ./${GEN_API_GO}/config.yaml ./${GEN_API_GO}/templates/
gen-dev-config: ## Generate a local development config file gen-dev-config: ## Generate a local development config file
@ -184,56 +207,38 @@ gen: gen-build gen-client-ts
## Web ## Web
######################### #########################
web-build: web-install ## Build the Authentik UI web: web-lint-fix web-lint node-check-compile ## Automatically fix formatting issues in the Authentik UI source code, lint the code, and compile it
cd web && npm run build
web: web-lint-fix web-lint web-check-compile ## Automatically fix formatting issues in the Authentik UI source code, lint the code, and compile it
web-install: ## Install the necessary libraries to build the Authentik UI
cd web && npm ci
web-test: ## Run tests for the Authentik UI web-test: ## Run tests for the Authentik UI
cd web && npm run test npm run test -w @goauthentik/web
web-watch: ## Build and watch the Authentik UI for changes, updating automatically web-watch: ## Build and watch the Authentik UI for changes, updating automatically
rm -rf web/dist/ npm run watch -w @goauthentik/web
mkdir web/dist/
touch web/dist/.gitkeep
cd web && npm run watch
web-storybook-watch: ## Build and run the storybook documentation server web-storybook-watch: ## Build and run the storybook documentation server
cd web && npm run storybook npm run storybook -w @goauthentik/web
web-lint-fix: web-lint-fix:
cd web && npm run prettier npm run prettier -w @goauthentik/web
web-lint: web-lint:
cd web && npm run lint npm run lint -w @goauthentik/web
cd web && npm run lit-analyse npm run lit-analyse -w @goauthentik/web
web-check-compile:
cd web && npm run tsc
web-i18n-extract: web-i18n-extract:
cd web && npm run extract-locales npm run extract-locales -w @goauthentik/web
######################### #########################
## Website ## Website
######################### #########################
website: website-lint-fix website-build ## Automatically fix formatting issues in the Authentik website/docs source code, lint the code, and compile it website: node-lint-fix website-build ## Automatically fix formatting issues in the Authentik website/docs source code, lint the code, and compile it
website-install:
cd website && npm ci
website-lint-fix: lint-codespell
cd website && npm run prettier
website-build: website-build:
cd website && npm run build npm run build -w @goauthentik/docs
website-watch: ## Build and watch the documentation website, updating automatically website-watch: ## Build and watch the documentation website, updating automatically
cd website && npm run watch npm run watch -w @goauthentik/docs
######################### #########################
## Docker ## Docker

View File

@ -7,7 +7,7 @@ from rest_framework.exceptions import ValidationError
from rest_framework.fields import CharField, DateTimeField from rest_framework.fields import CharField, DateTimeField
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.serializers import ListSerializer, ModelSerializer from rest_framework.serializers import ListSerializer
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from authentik.blueprints.models import BlueprintInstance from authentik.blueprints.models import BlueprintInstance
@ -15,7 +15,7 @@ from authentik.blueprints.v1.importer import Importer
from authentik.blueprints.v1.oci import OCI_PREFIX from authentik.blueprints.v1.oci import OCI_PREFIX
from authentik.blueprints.v1.tasks import apply_blueprint, blueprints_find_dict from authentik.blueprints.v1.tasks import apply_blueprint, blueprints_find_dict
from authentik.core.api.used_by import UsedByMixin from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import JSONDictField, PassiveSerializer from authentik.core.api.utils import JSONDictField, ModelSerializer, PassiveSerializer
from authentik.rbac.decorators import permission_required from authentik.rbac.decorators import permission_required

View File

@ -20,6 +20,8 @@ from rest_framework.serializers import (
raise_errors_on_nested_writes, raise_errors_on_nested_writes,
) )
from authentik.rbac.permissions import assign_initial_permissions
def is_dict(value: Any): def is_dict(value: Any):
"""Ensure a value is a dictionary, useful for JSONFields""" """Ensure a value is a dictionary, useful for JSONFields"""
@ -29,6 +31,14 @@ def is_dict(value: Any):
class ModelSerializer(BaseModelSerializer): class ModelSerializer(BaseModelSerializer):
def create(self, validated_data):
instance = super().create(validated_data)
request = self.context.get("request")
if request and hasattr(request, "user") and not request.user.is_anonymous:
assign_initial_permissions(request.user, instance)
return instance
def update(self, instance: Model, validated_data): def update(self, instance: Model, validated_data):
raise_errors_on_nested_writes("update", self, validated_data) raise_errors_on_nested_writes("update", self, validated_data)

View File

@ -2,20 +2,22 @@
{% get_current_language as LANGUAGE_CODE %} {% get_current_language as LANGUAGE_CODE %}
<script> <script>
window.authentik = { window.authentik = {
locale: "{{ LANGUAGE_CODE }}", locale: "{{ LANGUAGE_CODE }}",
config: JSON.parse('{{ config_json|escapejs }}'), config: JSON.parse("{{ config_json|escapejs }}" || "{}"),
brand: JSON.parse('{{ brand_json|escapejs }}'), brand: JSON.parse("{{ brand_json|escapejs }}" || "{}"),
versionFamily: "{{ version_family }}", versionFamily: "{{ version_family }}",
versionSubdomain: "{{ version_subdomain }}", versionSubdomain: "{{ version_subdomain }}",
build: "{{ build }}", build: "{{ build }}",
api: { api: {
base: "{{ base_url }}", base: "{{ base_url }}",
relBase: "{{ base_url_rel }}", relBase: "{{ base_url_rel }}",
}, },
}; };
{% if messages %}
window.addEventListener("DOMContentLoaded", function () { window.addEventListener("DOMContentLoaded", function () {
{% for message in messages %} {% for message in messages %}
window.dispatchEvent( window.dispatchEvent(
new CustomEvent("ak-message", { new CustomEvent("ak-message", {
bubbles: true, bubbles: true,
@ -26,6 +28,7 @@
}, },
}), }),
); );
{% endfor %} {% endfor %}
}); });
{% endif %}
</script> </script>

View File

@ -2,31 +2,79 @@
{% load i18n %} {% load i18n %}
{% load authentik_core %} {% load authentik_core %}
<!DOCTYPE html> <!doctype html>
<html> <html>
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
{# Darkreader breaks the site regardless of theme as its not compatible with webcomponents, and we default to a dark theme based on preferred colour-scheme #}
<meta name="darkreader-lock"> {% comment %}
<title>{% block title %}{% trans title|default:brand.branding_title %}{% endblock %}</title> Darkreader breaks the site regardless of theme as its not compatible with webcomponents, and we
<link rel="icon" href="{{ brand.branding_favicon_url }}"> default to a dark theme based on preferred colour-scheme
<link rel="shortcut icon" href="{{ brand.branding_favicon_url }}"> {% endcomment %}
{% block head_before %}
{% endblock %} <meta name="darkreader-lock" />
<link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}">
<style>{{ brand.branding_custom_css }}</style> <title>{% block title %}{% trans title|default:brand.branding_title %}{% endblock %}</title>
<script src="{% versioned_script 'dist/poly-%v.js' %}" type="module"></script>
<script src="{% versioned_script 'dist/standalone/loading/index-%v.js' %}" type="module"></script> <link rel="icon" href="{{ brand.branding_favicon_url }}" />
{% block head %} <link rel="shortcut icon" href="{{ brand.branding_favicon_url }}" />
{% endblock %}
<meta name="sentry-trace" content="{{ sentry_trace }}" /> {% block head_before %}
</head> {% endblock %}
<body>
{% block body %} <link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}" />
{% endblock %}
{% block scripts %} <style data-test-id="color-scheme">
{% endblock %} @media (prefers-color-scheme: dark) {
</body> :root {
color-scheme: dark light;
}
}
@media (prefers-color-scheme: light) {
:root {
color-scheme: light dark;
}
}
</style>
<style data-test-id="custom-branding-css">
{{ brand.branding_custom_css }}
</style>
<script src="{% versioned_script 'dist/poly-%v.js' %}" type="module"></script>
<script
src="{% versioned_script 'dist/standalone/loading/index-%v.js' %}"
type="module"
></script>
{% block head %}
{% endblock %}
<meta name="sentry-trace" content="{{ sentry_trace }}" />
</head>
<body>
{% block body %}{% endblock %}
{% block scripts %}{% endblock %}
<noscript>
<style>
body {
font-family: var(--ak-font-family-base), sans-serif;
}
</style>
<h1>
JavaScript is required to use
{% trans title|default:brand.branding_title %}
</h1>
<p>
Please enable JavaScript in your browser settings and reload the page. If you are using a
browser extension that blocks JavaScript, please disable it for this site.
</p>
</noscript>
</body>
</html> </html>

View File

@ -4,14 +4,16 @@
{% block head %} {% block head %}
<script src="{% versioned_script 'dist/admin/AdminInterface-%v.js' %}" type="module"></script> <script src="{% versioned_script 'dist/admin/AdminInterface-%v.js' %}" type="module"></script>
<meta name="theme-color" content="#18191a" media="(prefers-color-scheme: dark)">
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)"> <meta name="theme-color" content="#18191a" media="(prefers-color-scheme: dark)" />
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)" />
{% include "base/header_js.html" %} {% include "base/header_js.html" %}
{% endblock %} {% endblock %}
{% block body %} {% block body %}
<ak-message-container></ak-message-container> <ak-message-container></ak-message-container>
<ak-interface-admin> <ak-interface-admin>
<ak-loading></ak-loading> <ak-loading></ak-loading>
</ak-interface-admin> </ak-interface-admin>
{% endblock %} {% endblock %}

View File

@ -13,9 +13,14 @@
{% block card %} {% block card %}
<form method="POST" class="pf-c-form"> <form method="POST" class="pf-c-form">
<p>{% trans message %}</p> <p>{% trans message %}</p>
<a id="ak-back-home" href="{% url 'authentik_core:root-redirect' %}" class="pf-c-button pf-m-primary">
{% trans 'Go home' %} <a
</a> id="ak-back-home"
href="{% url 'authentik_core:root-redirect' %}"
class="pf-c-button pf-m-primary"
>
{% trans 'Go home' %}
</a>
</form> </form>
{% endblock %} {% endblock %}

View File

@ -4,14 +4,17 @@
{% block head %} {% block head %}
<script src="{% versioned_script 'dist/user/UserInterface-%v.js' %}" type="module"></script> <script src="{% versioned_script 'dist/user/UserInterface-%v.js' %}" type="module"></script>
<meta name="theme-color" content="#1c1e21" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#1c1e21" media="(prefers-color-scheme: dark)"> <meta name="theme-color" content="#1c1e21" media="(prefers-color-scheme: light)" />
<meta name="theme-color" content="#1c1e21" media="(prefers-color-scheme: dark)" />
{% include "base/header_js.html" %} {% include "base/header_js.html" %}
{% endblock %} {% endblock %}
{% block body %} {% block body %}
<ak-message-container></ak-message-container> <ak-message-container></ak-message-container>
<ak-interface-user> <ak-interface-user>
<ak-loading></ak-loading> <ak-loading></ak-loading>
</ak-interface-user> </ak-interface-user>
{% endblock %} {% endblock %}

View File

@ -5,78 +5,82 @@
{% block head_before %} {% block head_before %}
<link rel="prefetch" href="{{ request.brand.branding_default_flow_background_url }}" /> <link rel="prefetch" href="{{ request.brand.branding_default_flow_background_url }}" />
<link rel="stylesheet" type="text/css" href="{% static 'dist/patternfly.min.css' %}"> <link rel="stylesheet" type="text/css" href="{% static 'dist/patternfly.min.css' %}" />
<link rel="stylesheet" type="text/css" href="{% static 'dist/theme-dark.css' %}" media="(prefers-color-scheme: dark)"> <link
rel="stylesheet"
type="text/css"
href="{% static 'dist/theme-dark.css' %}"
media="(prefers-color-scheme: dark)"
/>
{% include "base/header_js.html" %} {% include "base/header_js.html" %}
{% endblock %} {% endblock %}
{% block head %} {% block head %}
<style> <style data-test-id="base-full-root-styles">
:root { :root {
--ak-flow-background: url("{{ request.brand.branding_default_flow_background_url }}"); --ak-flow-background: url("{{ request.brand.branding_default_flow_background_url }}");
--pf-c-background-image--BackgroundImage: var(--ak-flow-background); --pf-c-background-image--BackgroundImage: var(--ak-flow-background);
--pf-c-background-image--BackgroundImage-2x: var(--ak-flow-background); --pf-c-background-image--BackgroundImage-2x: var(--ak-flow-background);
--pf-c-background-image--BackgroundImage--sm: var(--ak-flow-background); --pf-c-background-image--BackgroundImage--sm: var(--ak-flow-background);
--pf-c-background-image--BackgroundImage--sm-2x: var(--ak-flow-background); --pf-c-background-image--BackgroundImage--sm-2x: var(--ak-flow-background);
--pf-c-background-image--BackgroundImage--lg: var(--ak-flow-background); --pf-c-background-image--BackgroundImage--lg: var(--ak-flow-background);
} }
/* Form with user */ /* Form with user */
.form-control-static { .form-control-static {
margin-top: var(--pf-global--spacer--sm); margin-top: var(--pf-global--spacer--sm);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
} }
.form-control-static .avatar { .form-control-static .avatar {
display: flex; display: flex;
align-items: center; align-items: center;
} }
.form-control-static img { .form-control-static img {
margin-right: var(--pf-global--spacer--xs); margin-right: var(--pf-global--spacer--xs);
} }
.form-control-static a { .form-control-static a {
padding-top: var(--pf-global--spacer--xs); padding-top: var(--pf-global--spacer--xs);
padding-bottom: var(--pf-global--spacer--xs); padding-bottom: var(--pf-global--spacer--xs);
line-height: var(--pf-global--spacer--xl); line-height: var(--pf-global--spacer--xl);
} }
</style> </style>
{% endblock %} {% endblock %}
{% block body %} {% block body %}
<div class="pf-c-background-image"> <div class="pf-c-background-image"></div>
</div>
<ak-message-container></ak-message-container> <ak-message-container></ak-message-container>
<div class="pf-c-login stacked"> <div class="pf-c-login stacked">
<div class="ak-login-container"> <div class="ak-login-container">
<main class="pf-c-login__main"> <main class="pf-c-login__main">
<div class="pf-c-login__main-header pf-c-brand ak-brand"> <div class="pf-c-login__main-header pf-c-brand ak-brand">
<img src="{{ brand.branding_logo_url }}" alt="authentik Logo" /> <img src="{{ brand.branding_logo_url }}" alt="authentik Logo" />
</div> </div>
<header class="pf-c-login__main-header"> <header class="pf-c-login__main-header">
<h1 class="pf-c-title pf-m-3xl"> <h1 class="pf-c-title pf-m-3xl">
{% block card_title %} {% block card_title %}
{% endblock %} {% endblock %}
</h1> </h1>
</header> </header>
<div class="pf-c-login__main-body"> <div class="pf-c-login__main-body">
{% block card %} {% block card %}
{% endblock %} {% endblock %}
</div> </div>
</main> </main>
<footer class="pf-c-login__footer"> <footer class="pf-c-login__footer">
<ul class="pf-c-list pf-m-inline"> <ul class="pf-c-list pf-m-inline">
{% for link in footer_links %} {% for link in footer_links %}
<li> <li>
<a href="{{ link.href }}">{{ link.name }}</a> <a href="{{ link.href }}">{{ link.name }}</a>
</li> </li>
{% endfor %} {% endfor %}
<li> <li>
<span> <span>
{% trans 'Powered by authentik' %} {% trans 'Powered by authentik' %}
</span> </span>
</li> </li>
</ul> </ul>
</footer> </footer>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -1,9 +1,17 @@
"""Test API Utils""" """Test API Utils"""
from rest_framework.exceptions import ValidationError from rest_framework.exceptions import ValidationError
from rest_framework.serializers import (
HyperlinkedModelSerializer,
)
from rest_framework.serializers import (
ModelSerializer as BaseModelSerializer,
)
from rest_framework.test import APITestCase from rest_framework.test import APITestCase
from authentik.core.api.utils import ModelSerializer as CustomModelSerializer
from authentik.core.api.utils import is_dict from authentik.core.api.utils import is_dict
from authentik.lib.utils.reflection import all_subclasses
class TestAPIUtils(APITestCase): class TestAPIUtils(APITestCase):
@ -14,3 +22,14 @@ class TestAPIUtils(APITestCase):
self.assertIsNone(is_dict({})) self.assertIsNone(is_dict({}))
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
is_dict("foo") is_dict("foo")
def test_all_serializers_descend_from_custom(self):
"""Test that every serializer we define descends from our own ModelSerializer"""
# Weirdly, there's only one serializer in `rest_framework` which descends from
# ModelSerializer: HyperlinkedModelSerializer
expected = {CustomModelSerializer, HyperlinkedModelSerializer}
actual = set(all_subclasses(BaseModelSerializer)) - set(
all_subclasses(CustomModelSerializer)
)
self.assertEqual(expected, actual)

View File

@ -4,10 +4,9 @@ from rest_framework.exceptions import PermissionDenied, ValidationError
from rest_framework.fields import CharField, ChoiceField, ListField, SerializerMethodField from rest_framework.fields import CharField, ChoiceField, ListField, SerializerMethodField
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from authentik.core.api.utils import PassiveSerializer from authentik.core.api.utils import ModelSerializer, PassiveSerializer
from authentik.enterprise.providers.ssf.models import ( from authentik.enterprise.providers.ssf.models import (
DeliveryMethods, DeliveryMethods,
EventTypes, EventTypes,

View File

@ -2,11 +2,11 @@
from rest_framework import mixins from rest_framework import mixins
from rest_framework.permissions import IsAdminUser from rest_framework.permissions import IsAdminUser
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import GenericViewSet, ModelViewSet from rest_framework.viewsets import GenericViewSet, ModelViewSet
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from authentik.core.api.used_by import UsedByMixin from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer
from authentik.enterprise.api import EnterpriseRequiredMixin from authentik.enterprise.api import EnterpriseRequiredMixin
from authentik.enterprise.stages.authenticator_endpoint_gdtc.models import ( from authentik.enterprise.stages.authenticator_endpoint_gdtc.models import (
AuthenticatorEndpointGDTCStage, AuthenticatorEndpointGDTCStage,

View File

@ -2,53 +2,52 @@
{% load i18n %} {% load i18n %}
{% load authentik_core %} {% load authentik_core %}
<!DOCTYPE html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
<title>{% block title %}{% trans title|default:brand.branding_title %}{% endblock %}</title> <title>{% block title %}{% trans title|default:brand.branding_title %}{% endblock %}</title>
<link rel="icon" href="{{ brand.branding_favicon_url }}"> <link rel="icon" href="{{ brand.branding_favicon_url }}" />
<link rel="shortcut icon" href="{{ brand.branding_favicon_url }}"> <link rel="shortcut icon" href="{{ brand.branding_favicon_url }}" />
{% block head_before %} {% block head_before %}
{% endblock %} {% endblock %}
<link rel="stylesheet" type="text/css" href="{% static 'dist/sfe/bootstrap.min.css' %}"> <link rel="stylesheet" type="text/css" href="{% static 'dist/sfe/bootstrap.min.css' %}" />
<meta name="sentry-trace" content="{{ sentry_trace }}" /> <meta name="sentry-trace" content="{{ sentry_trace }}" />
{% include "base/header_js.html" %} {% include "base/header_js.html" %}
<style> <style>
html, html,
body { body {
height: 100%; height: 100%;
} }
body { body {
background-image: url("{{ flow.background_url }}"); background-image: url("{{ flow.background_url }}");
background-repeat: no-repeat; background-repeat: no-repeat;
background-size: cover; background-size: cover;
} }
.card { .card {
padding: 3rem; padding: 3rem;
} }
.form-signin { .form-signin {
max-width: 330px; max-width: 330px;
padding: 1rem; padding: 1rem;
} }
.form-signin .form-floating:focus-within { .form-signin .form-floating:focus-within {
z-index: 2; z-index: 2;
} }
.brand-icon { .brand-icon {
max-width: 100%; max-width: 100%;
} }
</style> </style>
</head> </head>
<body class="d-flex align-items-center py-4 bg-body-tertiary"> <body class="d-flex align-items-center py-4 bg-body-tertiary">
<div class="card m-auto"> <div class="card m-auto">
<main class="form-signin w-100 m-auto" id="flow-sfe-container"> <main class="form-signin w-100 m-auto" id="flow-sfe-container"></main>
</main> <span class="mt-3 mb-0 text-muted text-center">{% trans 'Powered by authentik' %}</span>
<span class="mt-3 mb-0 text-muted text-center">{% trans 'Powered by authentik' %}</span> </div>
</div> <script src="{% static 'dist/sfe/index.js' %}"></script>
<script src="{% static 'dist/sfe/index.js' %}"></script> </body>
</body>
</html> </html>

View File

@ -1,34 +1,40 @@
{% extends "base/skeleton.html" %} {% extends "base/skeleton.html" %}
{% load static %} {% load static %}
{% load authentik_core %} {% load authentik_core %}
{% block head_before %} {% block head_before %}
{{ block.super }} {{ block.super }}
<link rel="prefetch" href="{{ flow.background_url }}" /> <link rel="prefetch" href="{{ flow.background_url }}" />
{% if flow.compatibility_mode and not inspector %} {% if flow.compatibility_mode and not inspector %}
<script>ShadyDOM = { force: !navigator.webdriver };</script>
{% endif %}
{% include "base/header_js.html" %}
<script> <script>
window.authentik.flow = { ShadyDOM = { force: !navigator.webdriver };
"layout": "{{ flow.layout }}", </script>
}; {% endif %}
{% include "base/header_js.html" %}
<script>
window.authentik.flow = {
layout: "{{ flow.layout }}",
};
</script> </script>
{% endblock %} {% endblock %}
{% block head %} {% block head %}
<script src="{% versioned_script 'dist/flow/FlowInterface-%v.js' %}" type="module"></script> <script src="{% versioned_script 'dist/flow/FlowInterface-%v.js' %}" type="module"></script>
<style>
:root { <style data-test-id="flow-root-styles">
:root {
--ak-flow-background: url("{{ flow.background_url }}"); --ak-flow-background: url("{{ flow.background_url }}");
} }
</style> </style>
{% endblock %} {% endblock %}
{% block body %} {% block body %}
<ak-message-container></ak-message-container> <ak-message-container></ak-message-container>
<ak-flow-executor flowSlug="{{ flow.slug }}"> <ak-flow-executor flowSlug="{{ flow.slug }}">
<ak-loading></ak-loading> <ak-loading></ak-loading>
</ak-flow-executor> </ak-flow-executor>
{% endblock %} {% endblock %}

View File

@ -356,6 +356,14 @@ def redis_url(db: int) -> str:
def django_db_config(config: ConfigLoader | None = None) -> dict: def django_db_config(config: ConfigLoader | None = None) -> dict:
if not config: if not config:
config = CONFIG config = CONFIG
pool_options = False
use_pool = config.get_bool("postgresql.use_pool", False)
if use_pool:
pool_options = config.get_dict_from_b64_json("postgresql.pool_options", True)
if not pool_options:
pool_options = True
db = { db = {
"default": { "default": {
"ENGINE": "authentik.root.db", "ENGINE": "authentik.root.db",
@ -369,6 +377,7 @@ def django_db_config(config: ConfigLoader | None = None) -> dict:
"sslrootcert": config.get("postgresql.sslrootcert"), "sslrootcert": config.get("postgresql.sslrootcert"),
"sslcert": config.get("postgresql.sslcert"), "sslcert": config.get("postgresql.sslcert"),
"sslkey": config.get("postgresql.sslkey"), "sslkey": config.get("postgresql.sslkey"),
"pool": pool_options,
}, },
"CONN_MAX_AGE": config.get_optional_int("postgresql.conn_max_age", 0), "CONN_MAX_AGE": config.get_optional_int("postgresql.conn_max_age", 0),
"CONN_HEALTH_CHECKS": config.get_bool("postgresql.conn_health_checks", False), "CONN_HEALTH_CHECKS": config.get_bool("postgresql.conn_health_checks", False),

View File

@ -21,6 +21,7 @@ postgresql:
user: authentik user: authentik
port: 5432 port: 5432
password: "env://POSTGRES_PASSWORD" password: "env://POSTGRES_PASSWORD"
use_pool: False
test: test:
name: test_authentik name: test_authentik
default_schema: public default_schema: public

View File

@ -217,6 +217,7 @@ class TestConfig(TestCase):
"HOST": "foo", "HOST": "foo",
"NAME": "foo", "NAME": "foo",
"OPTIONS": { "OPTIONS": {
"pool": False,
"sslcert": "foo", "sslcert": "foo",
"sslkey": "foo", "sslkey": "foo",
"sslmode": "foo", "sslmode": "foo",
@ -267,6 +268,7 @@ class TestConfig(TestCase):
"HOST": "foo", "HOST": "foo",
"NAME": "foo", "NAME": "foo",
"OPTIONS": { "OPTIONS": {
"pool": False,
"sslcert": "foo", "sslcert": "foo",
"sslkey": "foo", "sslkey": "foo",
"sslmode": "foo", "sslmode": "foo",
@ -285,6 +287,7 @@ class TestConfig(TestCase):
"HOST": "bar", "HOST": "bar",
"NAME": "foo", "NAME": "foo",
"OPTIONS": { "OPTIONS": {
"pool": False,
"sslcert": "foo", "sslcert": "foo",
"sslkey": "foo", "sslkey": "foo",
"sslmode": "foo", "sslmode": "foo",
@ -333,6 +336,7 @@ class TestConfig(TestCase):
"HOST": "foo", "HOST": "foo",
"NAME": "foo", "NAME": "foo",
"OPTIONS": { "OPTIONS": {
"pool": False,
"sslcert": "foo", "sslcert": "foo",
"sslkey": "foo", "sslkey": "foo",
"sslmode": "foo", "sslmode": "foo",
@ -351,6 +355,7 @@ class TestConfig(TestCase):
"HOST": "bar", "HOST": "bar",
"NAME": "foo", "NAME": "foo",
"OPTIONS": { "OPTIONS": {
"pool": False,
"sslcert": "foo", "sslcert": "foo",
"sslkey": "foo", "sslkey": "foo",
"sslmode": "foo", "sslmode": "foo",
@ -394,6 +399,7 @@ class TestConfig(TestCase):
"HOST": "foo", "HOST": "foo",
"NAME": "foo", "NAME": "foo",
"OPTIONS": { "OPTIONS": {
"pool": False,
"sslcert": "foo", "sslcert": "foo",
"sslkey": "foo", "sslkey": "foo",
"sslmode": "foo", "sslmode": "foo",
@ -412,6 +418,7 @@ class TestConfig(TestCase):
"HOST": "bar", "HOST": "bar",
"NAME": "foo", "NAME": "foo",
"OPTIONS": { "OPTIONS": {
"pool": False,
"sslcert": "foo", "sslcert": "foo",
"sslkey": "foo", "sslkey": "foo",
"sslmode": "foo", "sslmode": "foo",
@ -451,6 +458,7 @@ class TestConfig(TestCase):
"HOST": "foo", "HOST": "foo",
"NAME": "foo", "NAME": "foo",
"OPTIONS": { "OPTIONS": {
"pool": False,
"sslcert": "foo", "sslcert": "foo",
"sslkey": "foo", "sslkey": "foo",
"sslmode": "foo", "sslmode": "foo",
@ -469,6 +477,7 @@ class TestConfig(TestCase):
"HOST": "bar", "HOST": "bar",
"NAME": "foo", "NAME": "foo",
"OPTIONS": { "OPTIONS": {
"pool": False,
"sslcert": "bar", "sslcert": "bar",
"sslkey": "foo", "sslkey": "foo",
"sslmode": "foo", "sslmode": "foo",
@ -484,3 +493,87 @@ class TestConfig(TestCase):
}, },
}, },
) )
def test_db_pool(self):
"""Test DB Config with pool"""
config = ConfigLoader()
config.set("postgresql.host", "foo")
config.set("postgresql.name", "foo")
config.set("postgresql.user", "foo")
config.set("postgresql.password", "foo")
config.set("postgresql.port", "foo")
config.set("postgresql.test.name", "foo")
config.set("postgresql.use_pool", True)
conf = django_db_config(config)
self.assertEqual(
conf,
{
"default": {
"ENGINE": "authentik.root.db",
"HOST": "foo",
"NAME": "foo",
"OPTIONS": {
"pool": True,
"sslcert": None,
"sslkey": None,
"sslmode": None,
"sslrootcert": None,
},
"PASSWORD": "foo",
"PORT": "foo",
"TEST": {"NAME": "foo"},
"USER": "foo",
"CONN_MAX_AGE": 0,
"CONN_HEALTH_CHECKS": False,
"DISABLE_SERVER_SIDE_CURSORS": False,
}
},
)
def test_db_pool_options(self):
"""Test DB Config with pool"""
config = ConfigLoader()
config.set("postgresql.host", "foo")
config.set("postgresql.name", "foo")
config.set("postgresql.user", "foo")
config.set("postgresql.password", "foo")
config.set("postgresql.port", "foo")
config.set("postgresql.test.name", "foo")
config.set("postgresql.use_pool", True)
config.set(
"postgresql.pool_options",
base64.b64encode(
dumps(
{
"max_size": 15,
}
).encode()
).decode(),
)
conf = django_db_config(config)
self.assertEqual(
conf,
{
"default": {
"ENGINE": "authentik.root.db",
"HOST": "foo",
"NAME": "foo",
"OPTIONS": {
"pool": {
"max_size": 15,
},
"sslcert": None,
"sslkey": None,
"sslmode": None,
"sslrootcert": None,
},
"PASSWORD": "foo",
"PORT": "foo",
"TEST": {"NAME": "foo"},
"USER": "foo",
"CONN_MAX_AGE": 0,
"CONN_HEALTH_CHECKS": False,
"DISABLE_SERVER_SIDE_CURSORS": False,
}
},
)

View File

@ -66,7 +66,9 @@ class GeoIPPolicy(Policy):
if not static_results and not dynamic_results: if not static_results and not dynamic_results:
return PolicyResult(True) return PolicyResult(True)
passing = any(r.passing for r in static_results) and all(r.passing for r in dynamic_results) static_passing = any(r.passing for r in static_results) if static_results else True
dynamic_passing = all(r.passing for r in dynamic_results)
passing = static_passing and dynamic_passing
messages = chain( messages = chain(
*[r.messages for r in static_results], *[r.messages for r in dynamic_results] *[r.messages for r in static_results], *[r.messages for r in dynamic_results]
) )
@ -113,13 +115,19 @@ class GeoIPPolicy(Policy):
to previous authentication requests""" to previous authentication requests"""
# Get previous login event and GeoIP data # Get previous login event and GeoIP data
previous_logins = Event.objects.filter( previous_logins = Event.objects.filter(
action=EventAction.LOGIN, user__pk=request.user.pk, context__geo__isnull=False action=EventAction.LOGIN,
user__pk=request.user.pk, # context__geo__isnull=False
).order_by("-created")[: self.history_login_count] ).order_by("-created")[: self.history_login_count]
_now = now() _now = now()
geoip_data: GeoIPDict | None = request.context.get("geoip") geoip_data: GeoIPDict | None = request.context.get("geoip")
if not geoip_data: if not geoip_data:
return PolicyResult(False) return PolicyResult(False)
if not previous_logins.exists():
return PolicyResult(True)
result = False
for previous_login in previous_logins: for previous_login in previous_logins:
if "geo" not in previous_login.context:
continue
previous_login_geoip: GeoIPDict = previous_login.context["geo"] previous_login_geoip: GeoIPDict = previous_login.context["geo"]
# Figure out distance # Figure out distance
@ -142,7 +150,8 @@ class GeoIPPolicy(Policy):
(MAX_DISTANCE_HOUR_KM * rel_time_hours) + self.distance_tolerance_km (MAX_DISTANCE_HOUR_KM * rel_time_hours) + self.distance_tolerance_km
): ):
return PolicyResult(False, _("Distance is further than possible.")) return PolicyResult(False, _("Distance is further than possible."))
return PolicyResult(True) result = True
return PolicyResult(result)
class Meta(Policy.PolicyMeta): class Meta(Policy.PolicyMeta):
verbose_name = _("GeoIP Policy") verbose_name = _("GeoIP Policy")

View File

@ -163,7 +163,7 @@ class TestGeoIPPolicy(TestCase):
result: PolicyResult = policy.passes(self.request) result: PolicyResult = policy.passes(self.request)
self.assertFalse(result.passing) self.assertFalse(result.passing)
def test_history_impossible_travel(self): def test_history_impossible_travel_failing(self):
"""Test history checks""" """Test history checks"""
Event.objects.create( Event.objects.create(
action=EventAction.LOGIN, action=EventAction.LOGIN,
@ -181,6 +181,24 @@ class TestGeoIPPolicy(TestCase):
result: PolicyResult = policy.passes(self.request) result: PolicyResult = policy.passes(self.request)
self.assertFalse(result.passing) self.assertFalse(result.passing)
def test_history_impossible_travel_passing(self):
"""Test history checks"""
Event.objects.create(
action=EventAction.LOGIN,
user=get_user(self.user),
context={
# Random location in Canada
"geo": {"lat": 55.868351, "long": -104.441011},
},
)
# Same location
self.request.context["geoip"] = {"lat": 55.868351, "long": -104.441011}
policy = GeoIPPolicy.objects.create(check_impossible_travel=True)
result: PolicyResult = policy.passes(self.request)
self.assertTrue(result.passing)
def test_history_no_geoip(self): def test_history_no_geoip(self):
"""Test history checks (previous login with no geoip data)""" """Test history checks (previous login with no geoip data)"""
Event.objects.create( Event.objects.create(
@ -195,3 +213,18 @@ class TestGeoIPPolicy(TestCase):
result: PolicyResult = policy.passes(self.request) result: PolicyResult = policy.passes(self.request)
self.assertFalse(result.passing) self.assertFalse(result.passing)
def test_impossible_travel_no_geoip(self):
"""Test impossible travel checks (previous login with no geoip data)"""
Event.objects.create(
action=EventAction.LOGIN,
user=get_user(self.user),
context={},
)
# Random location in Poland
self.request.context["geoip"] = {"lat": 50.950613, "long": 20.363679}
policy = GeoIPPolicy.objects.create(check_impossible_travel=True)
result: PolicyResult = policy.passes(self.request)
self.assertFalse(result.passing)

View File

@ -4,10 +4,13 @@
{% block head %} {% block head %}
<script src="{% versioned_script 'dist/rac/index-%v.js' %}" type="module"></script> <script src="{% versioned_script 'dist/rac/index-%v.js' %}" type="module"></script>
<meta name="theme-color" content="#18191a" media="(prefers-color-scheme: dark)">
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)"> <meta name="theme-color" content="#18191a" media="(prefers-color-scheme: dark)" />
<link rel="icon" href="{{ tenant.branding_favicon_url }}"> <meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)" />
<link rel="shortcut icon" href="{{ tenant.branding_favicon_url }}">
<link rel="icon" href="{{ tenant.branding_favicon_url }}" />
<link rel="shortcut icon" href="{{ tenant.branding_favicon_url }}" />
{% include "base/header_js.html" %} {% include "base/header_js.html" %}
{% endblock %} {% endblock %}

View File

@ -0,0 +1,41 @@
"""RBAC Initial Permissions"""
from rest_framework.serializers import ListSerializer
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer
from authentik.rbac.api.rbac import PermissionSerializer
from authentik.rbac.models import InitialPermissions
class InitialPermissionsSerializer(ModelSerializer):
"""InitialPermissions serializer"""
permissions_obj = ListSerializer(
child=PermissionSerializer(),
read_only=True,
source="permissions",
required=False,
)
class Meta:
model = InitialPermissions
fields = [
"pk",
"name",
"mode",
"role",
"permissions",
"permissions_obj",
]
class InitialPermissionsViewSet(UsedByMixin, ModelViewSet):
"""InitialPermissions viewset"""
queryset = InitialPermissions.objects.all()
serializer_class = InitialPermissionsSerializer
search_fields = ["name"]
ordering = ["name"]
filterset_fields = ["name"]

View File

@ -0,0 +1,39 @@
# Generated by Django 5.0.13 on 2025-04-07 13:05
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("auth", "0012_alter_user_first_name_max_length"),
("authentik_rbac", "0004_alter_systempermission_options"),
]
operations = [
migrations.CreateModel(
name="InitialPermissions",
fields=[
(
"id",
models.AutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
("name", models.TextField(max_length=150, unique=True)),
("mode", models.CharField(choices=[("user", "User"), ("role", "Role")])),
("permissions", models.ManyToManyField(blank=True, to="auth.permission")),
(
"role",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="authentik_rbac.role"
),
),
],
options={
"verbose_name": "Initial Permissions",
"verbose_name_plural": "Initial Permissions",
},
),
]

View File

@ -3,6 +3,7 @@
from uuid import uuid4 from uuid import uuid4
from django.contrib.auth.management import _get_all_permissions from django.contrib.auth.management import _get_all_permissions
from django.contrib.auth.models import Permission
from django.db import models from django.db import models
from django.db.transaction import atomic from django.db.transaction import atomic
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@ -75,6 +76,35 @@ class Role(SerializerModel):
] ]
class InitialPermissionsMode(models.TextChoices):
"""Determines which entity the initial permissions are assigned to."""
USER = "user", _("User")
ROLE = "role", _("Role")
class InitialPermissions(SerializerModel):
"""Assigns permissions for newly created objects."""
name = models.TextField(max_length=150, unique=True)
mode = models.CharField(choices=InitialPermissionsMode.choices)
role = models.ForeignKey(Role, on_delete=models.CASCADE)
permissions = models.ManyToManyField(Permission, blank=True)
@property
def serializer(self) -> type[BaseSerializer]:
from authentik.rbac.api.initial_permissions import InitialPermissionsSerializer
return InitialPermissionsSerializer
def __str__(self) -> str:
return f"Initial Permissions for Role #{self.role_id}, applying to #{self.mode}"
class Meta:
verbose_name = _("Initial Permissions")
verbose_name_plural = _("Initial Permissions")
class SystemPermission(models.Model): class SystemPermission(models.Model):
"""System-wide permissions that are not related to any direct """System-wide permissions that are not related to any direct
database model""" database model"""

View File

@ -1,9 +1,13 @@
"""RBAC Permissions""" """RBAC Permissions"""
from django.contrib.contenttypes.models import ContentType
from django.db.models import Model from django.db.models import Model
from guardian.shortcuts import assign_perm
from rest_framework.permissions import BasePermission, DjangoObjectPermissions from rest_framework.permissions import BasePermission, DjangoObjectPermissions
from rest_framework.request import Request from rest_framework.request import Request
from authentik.rbac.models import InitialPermissions, InitialPermissionsMode
class ObjectPermissions(DjangoObjectPermissions): class ObjectPermissions(DjangoObjectPermissions):
"""RBAC Permissions""" """RBAC Permissions"""
@ -51,3 +55,20 @@ def HasPermission(*perm: str) -> type[BasePermission]:
return bool(request.user and request.user.has_perms(perm)) return bool(request.user and request.user.has_perms(perm))
return checker return checker
# TODO: add `user: User` type annotation without circular dependencies.
# The author of this function isn't proficient/patient enough to do it.
def assign_initial_permissions(user, instance: Model):
# Performance here should not be an issue, but if needed, there are many optimization routes
initial_permissions_list = InitialPermissions.objects.filter(role__group__in=user.groups.all())
for initial_permissions in initial_permissions_list:
for permission in initial_permissions.permissions.all():
if permission.content_type != ContentType.objects.get_for_model(instance):
continue
assign_to = (
user
if initial_permissions.mode == InitialPermissionsMode.USER
else initial_permissions.role.group
)
assign_perm(permission, assign_to, instance)

View File

@ -0,0 +1,116 @@
"""Test InitialPermissions"""
from django.contrib.auth.models import Permission
from guardian.shortcuts import assign_perm
from rest_framework.reverse import reverse
from rest_framework.test import APITestCase
from authentik.core.models import Group
from authentik.core.tests.utils import create_test_user
from authentik.lib.generators import generate_id
from authentik.rbac.models import InitialPermissions, InitialPermissionsMode, Role
from authentik.stages.dummy.models import DummyStage
class TestInitialPermissions(APITestCase):
"""Test InitialPermissions"""
def setUp(self) -> None:
self.user = create_test_user()
self.same_role_user = create_test_user()
self.different_role_user = create_test_user()
self.role = Role.objects.create(name=generate_id())
self.different_role = Role.objects.create(name=generate_id())
self.group = Group.objects.create(name=generate_id())
self.different_group = Group.objects.create(name=generate_id())
self.group.roles.add(self.role)
self.group.users.add(self.user, self.same_role_user)
self.different_group.roles.add(self.different_role)
self.different_group.users.add(self.different_role_user)
self.ip = InitialPermissions.objects.create(
name=generate_id(), mode=InitialPermissionsMode.USER, role=self.role
)
self.view_role = Permission.objects.filter(codename="view_role").first()
self.ip.permissions.add(self.view_role)
assign_perm("authentik_rbac.add_role", self.user)
self.client.force_login(self.user)
def test_different_role(self):
"""InitialPermissions for different role does nothing"""
self.ip.role = self.different_role
self.ip.save()
self.client.post(reverse("authentik_api:roles-list"), {"name": "test-role"})
role = Role.objects.filter(name="test-role").first()
self.assertFalse(self.user.has_perm("authentik_rbac.view_role", role))
def test_different_model(self):
"""InitialPermissions for different model does nothing"""
assign_perm("authentik_stages_dummy.add_dummystage", self.user)
self.client.post(
reverse("authentik_api:stages-dummy-list"), {"name": "test-stage", "throw-error": False}
)
role = Role.objects.filter(name="test-role").first()
self.assertFalse(self.user.has_perm("authentik_rbac.view_role", role))
stage = DummyStage.objects.filter(name="test-stage").first()
self.assertFalse(self.user.has_perm("authentik_stages_dummy.view_dummystage", stage))
def test_mode_user(self):
"""InitialPermissions adds user permission in user mode"""
self.client.post(reverse("authentik_api:roles-list"), {"name": "test-role"})
role = Role.objects.filter(name="test-role").first()
self.assertTrue(self.user.has_perm("authentik_rbac.view_role", role))
self.assertFalse(self.same_role_user.has_perm("authentik_rbac.view_role", role))
def test_mode_role(self):
"""InitialPermissions adds role permission in role mode"""
self.ip.mode = InitialPermissionsMode.ROLE
self.ip.save()
self.client.post(reverse("authentik_api:roles-list"), {"name": "test-role"})
role = Role.objects.filter(name="test-role").first()
self.assertTrue(self.user.has_perm("authentik_rbac.view_role", role))
self.assertTrue(self.same_role_user.has_perm("authentik_rbac.view_role", role))
def test_many_permissions(self):
"""InitialPermissions can add multiple permissions"""
change_role = Permission.objects.filter(codename="change_role").first()
self.ip.permissions.add(change_role)
self.client.post(reverse("authentik_api:roles-list"), {"name": "test-role"})
role = Role.objects.filter(name="test-role").first()
self.assertTrue(self.user.has_perm("authentik_rbac.view_role", role))
self.assertTrue(self.user.has_perm("authentik_rbac.change_role", role))
def test_permissions_separated_by_role(self):
"""When the triggering user is part of two different roles with InitialPermissions in role
mode, it only adds permissions to the relevant role."""
self.ip.mode = InitialPermissionsMode.ROLE
self.ip.save()
different_ip = InitialPermissions.objects.create(
name=generate_id(), mode=InitialPermissionsMode.ROLE, role=self.different_role
)
change_role = Permission.objects.filter(codename="change_role").first()
different_ip.permissions.add(change_role)
self.different_group.users.add(self.user)
self.client.post(reverse("authentik_api:roles-list"), {"name": "test-role"})
role = Role.objects.filter(name="test-role").first()
self.assertTrue(self.user.has_perm("authentik_rbac.view_role", role))
self.assertTrue(self.same_role_user.has_perm("authentik_rbac.view_role", role))
self.assertFalse(self.different_role_user.has_perm("authentik_rbac.view_role", role))
self.assertTrue(self.user.has_perm("authentik_rbac.change_role", role))
self.assertFalse(self.same_role_user.has_perm("authentik_rbac.change_role", role))
self.assertTrue(self.different_role_user.has_perm("authentik_rbac.change_role", role))

View File

@ -1,5 +1,6 @@
"""RBAC API urls""" """RBAC API urls"""
from authentik.rbac.api.initial_permissions import InitialPermissionsViewSet
from authentik.rbac.api.rbac import RBACPermissionViewSet from authentik.rbac.api.rbac import RBACPermissionViewSet
from authentik.rbac.api.rbac_assigned_by_roles import RoleAssignedPermissionViewSet from authentik.rbac.api.rbac_assigned_by_roles import RoleAssignedPermissionViewSet
from authentik.rbac.api.rbac_assigned_by_users import UserAssignedPermissionViewSet from authentik.rbac.api.rbac_assigned_by_users import UserAssignedPermissionViewSet
@ -21,5 +22,6 @@ api_urlpatterns = [
("rbac/permissions/users", UserPermissionViewSet, "permissions-users"), ("rbac/permissions/users", UserPermissionViewSet, "permissions-users"),
("rbac/permissions/roles", RolePermissionViewSet, "permissions-roles"), ("rbac/permissions/roles", RolePermissionViewSet, "permissions-roles"),
("rbac/permissions", RBACPermissionViewSet), ("rbac/permissions", RBACPermissionViewSet),
("rbac/roles", RoleViewSet), ("rbac/roles", RoleViewSet, "roles"),
("rbac/initial_permissions", InitialPermissionsViewSet, "initial-permissions"),
] ]

View File

@ -130,6 +130,7 @@ class OAuthSourceSerializer(SourceSerializer):
"oidc_well_known_url", "oidc_well_known_url",
"oidc_jwks_url", "oidc_jwks_url",
"oidc_jwks", "oidc_jwks",
"authorization_code_auth_method",
] ]
extra_kwargs = { extra_kwargs = {
"consumer_secret": {"write_only": True}, "consumer_secret": {"write_only": True},

View File

@ -6,11 +6,15 @@ from urllib.parse import parse_qsl
from django.utils.crypto import constant_time_compare, get_random_string from django.utils.crypto import constant_time_compare, get_random_string
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from requests.auth import AuthBase, HTTPBasicAuth
from requests.exceptions import RequestException from requests.exceptions import RequestException
from requests.models import Response from requests.models import Response
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from authentik.sources.oauth.clients.base import BaseOAuthClient from authentik.sources.oauth.clients.base import BaseOAuthClient
from authentik.sources.oauth.models import (
AuthorizationCodeAuthMethod,
)
LOGGER = get_logger() LOGGER = get_logger()
SESSION_KEY_OAUTH_PKCE = "authentik/sources/oauth/pkce" SESSION_KEY_OAUTH_PKCE = "authentik/sources/oauth/pkce"
@ -55,6 +59,30 @@ class OAuth2Client(BaseOAuthClient):
"""Get client secret""" """Get client secret"""
return self.source.consumer_secret return self.source.consumer_secret
def get_access_token_args(self, callback: str, code: str) -> dict[str, Any]:
args = {
"redirect_uri": callback,
"code": code,
"grant_type": "authorization_code",
}
if SESSION_KEY_OAUTH_PKCE in self.request.session:
args["code_verifier"] = self.request.session[SESSION_KEY_OAUTH_PKCE]
if (
self.source.source_type.authorization_code_auth_method
== AuthorizationCodeAuthMethod.POST_BODY
):
args["client_id"] = self.get_client_id()
args["client_secret"] = self.get_client_secret()
return args
def get_access_token_auth(self) -> AuthBase | None:
if (
self.source.source_type.authorization_code_auth_method
== AuthorizationCodeAuthMethod.BASIC_AUTH
):
return HTTPBasicAuth(self.get_client_id(), self.get_client_secret())
return None
def get_access_token(self, **request_kwargs) -> dict[str, Any] | None: def get_access_token(self, **request_kwargs) -> dict[str, Any] | None:
"""Fetch access token from callback request.""" """Fetch access token from callback request."""
callback = self.request.build_absolute_uri(self.callback or self.request.path) callback = self.request.build_absolute_uri(self.callback or self.request.path)
@ -67,13 +95,6 @@ class OAuth2Client(BaseOAuthClient):
error = self.get_request_arg("error", None) error = self.get_request_arg("error", None)
error_desc = self.get_request_arg("error_description", None) error_desc = self.get_request_arg("error_description", None)
return {"error": error_desc or error or _("No token received.")} return {"error": error_desc or error or _("No token received.")}
args = {
"redirect_uri": callback,
"code": code,
"grant_type": "authorization_code",
}
if SESSION_KEY_OAUTH_PKCE in self.request.session:
args["code_verifier"] = self.request.session[SESSION_KEY_OAUTH_PKCE]
try: try:
access_token_url = self.source.source_type.access_token_url or "" access_token_url = self.source.source_type.access_token_url or ""
if self.source.source_type.urls_customizable and self.source.access_token_url: if self.source.source_type.urls_customizable and self.source.access_token_url:
@ -81,8 +102,8 @@ class OAuth2Client(BaseOAuthClient):
response = self.do_request( response = self.do_request(
"post", "post",
access_token_url, access_token_url,
auth=(self.get_client_id(), self.get_client_secret()), auth=self.get_access_token_auth(),
data=args, data=self.get_access_token_args(callback, code),
headers=self._default_headers, headers=self._default_headers,
**request_kwargs, **request_kwargs,
) )

View File

@ -0,0 +1,25 @@
# Generated by Django 5.0.14 on 2025-04-11 18:09
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_sources_oauth", "0009_migrate_useroauthsourceconnection_identifier"),
]
operations = [
migrations.AddField(
model_name="oauthsource",
name="authorization_code_auth_method",
field=models.TextField(
choices=[
("basic_auth", "HTTP Basic Authentication"),
("post_body", "Include the client ID and secret as request parameters"),
],
default="basic_auth",
help_text="How to perform authentication during an authorization_code token request flow",
),
),
]

View File

@ -21,6 +21,11 @@ if TYPE_CHECKING:
from authentik.sources.oauth.types.registry import SourceType from authentik.sources.oauth.types.registry import SourceType
class AuthorizationCodeAuthMethod(models.TextChoices):
BASIC_AUTH = "basic_auth", _("HTTP Basic Authentication")
POST_BODY = "post_body", _("Include the client ID and secret as request parameters")
class OAuthSource(NonCreatableType, Source): class OAuthSource(NonCreatableType, Source):
"""Login using a Generic OAuth provider.""" """Login using a Generic OAuth provider."""
@ -61,6 +66,14 @@ class OAuthSource(NonCreatableType, Source):
oidc_jwks_url = models.TextField(default="", blank=True) oidc_jwks_url = models.TextField(default="", blank=True)
oidc_jwks = models.JSONField(default=dict, blank=True) oidc_jwks = models.JSONField(default=dict, blank=True)
authorization_code_auth_method = models.TextField(
choices=AuthorizationCodeAuthMethod.choices,
default=AuthorizationCodeAuthMethod.BASIC_AUTH,
help_text=_(
"How to perform authentication during an authorization_code token request flow"
),
)
@property @property
def source_type(self) -> type["SourceType"]: def source_type(self) -> type["SourceType"]:
"""Return the provider instance for this source""" """Return the provider instance for this source"""

View File

@ -0,0 +1,69 @@
from django.test import RequestFactory, TestCase
from guardian.shortcuts import get_anonymous_user
from authentik.lib.generators import generate_id
from authentik.sources.oauth.clients.oauth2 import OAuth2Client
from authentik.sources.oauth.models import AuthorizationCodeAuthMethod, OAuthSource
from authentik.sources.oauth.types.oidc import OpenIDConnectClient
class TestOAuthClient(TestCase):
"""OAuth Source tests"""
def setUp(self):
self.source = OAuthSource.objects.create(
name="test",
slug="test",
provider_type="openidconnect",
authorization_url="",
profile_url="",
consumer_key=generate_id(),
)
self.factory = RequestFactory()
def test_client_post_body_auth(self):
"""Test login_challenge"""
self.source.provider_type = "apple"
self.source.save()
request = self.factory.get("/")
request.session = {}
request.user = get_anonymous_user()
client = OAuth2Client(self.source, request)
self.assertIsNone(client.get_access_token_auth())
args = client.get_access_token_args("", "")
self.assertIn("client_id", args)
self.assertIn("client_secret", args)
def test_client_basic_auth(self):
"""Test login_challenge"""
self.source.provider_type = "reddit"
self.source.save()
request = self.factory.get("/")
request.session = {}
request.user = get_anonymous_user()
client = OAuth2Client(self.source, request)
self.assertIsNotNone(client.get_access_token_auth())
args = client.get_access_token_args("", "")
self.assertNotIn("client_id", args)
self.assertNotIn("client_secret", args)
def test_client_openid_auth(self):
"""Test login_challenge"""
request = self.factory.get("/")
request.session = {}
request.user = get_anonymous_user()
client = OpenIDConnectClient(self.source, request)
self.assertIsNotNone(client.get_access_token_auth())
args = client.get_access_token_args("", "")
self.assertNotIn("client_id", args)
self.assertNotIn("client_secret", args)
self.source.authorization_code_auth_method = AuthorizationCodeAuthMethod.POST_BODY
self.source.save()
client = OpenIDConnectClient(self.source, request)
self.assertIsNone(client.get_access_token_auth())
args = client.get_access_token_args("", "")
self.assertIn("client_id", args)
self.assertIn("client_secret", args)

View File

@ -11,7 +11,7 @@ from structlog.stdlib import get_logger
from authentik.flows.challenge import Challenge, ChallengeResponse from authentik.flows.challenge import Challenge, ChallengeResponse
from authentik.sources.oauth.clients.oauth2 import OAuth2Client from authentik.sources.oauth.clients.oauth2 import OAuth2Client
from authentik.sources.oauth.models import OAuthSource from authentik.sources.oauth.models import AuthorizationCodeAuthMethod, OAuthSource
from authentik.sources.oauth.types.registry import SourceType, registry from authentik.sources.oauth.types.registry import SourceType, registry
from authentik.sources.oauth.views.callback import OAuthCallback from authentik.sources.oauth.views.callback import OAuthCallback
from authentik.sources.oauth.views.redirect import OAuthRedirect from authentik.sources.oauth.views.redirect import OAuthRedirect
@ -105,6 +105,8 @@ class AppleType(SourceType):
access_token_url = "https://appleid.apple.com/auth/token" # nosec access_token_url = "https://appleid.apple.com/auth/token" # nosec
profile_url = "" profile_url = ""
authorization_code_auth_method = AuthorizationCodeAuthMethod.POST_BODY
def login_challenge(self, source: OAuthSource, request: HttpRequest) -> Challenge: def login_challenge(self, source: OAuthSource, request: HttpRequest) -> Challenge:
"""Pre-general all the things required for the JS SDK""" """Pre-general all the things required for the JS SDK"""
apple_client = AppleOAuthClient( apple_client = AppleOAuthClient(

View File

@ -6,6 +6,7 @@ from requests import RequestException
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from authentik.sources.oauth.clients.oauth2 import UserprofileHeaderAuthClient from authentik.sources.oauth.clients.oauth2 import UserprofileHeaderAuthClient
from authentik.sources.oauth.models import AuthorizationCodeAuthMethod
from authentik.sources.oauth.types.oidc import OpenIDConnectOAuth2Callback from authentik.sources.oauth.types.oidc import OpenIDConnectOAuth2Callback
from authentik.sources.oauth.types.registry import SourceType, registry from authentik.sources.oauth.types.registry import SourceType, registry
from authentik.sources.oauth.views.redirect import OAuthRedirect from authentik.sources.oauth.views.redirect import OAuthRedirect
@ -77,6 +78,8 @@ class AzureADType(SourceType):
) )
oidc_jwks_url = "https://login.microsoftonline.com/common/discovery/keys" oidc_jwks_url = "https://login.microsoftonline.com/common/discovery/keys"
authorization_code_auth_method = AuthorizationCodeAuthMethod.POST_BODY
def get_base_user_properties(self, info: dict[str, Any], **kwargs) -> dict[str, Any]: def get_base_user_properties(self, info: dict[str, Any], **kwargs) -> dict[str, Any]:
mail = info.get("mail", None) or info.get("otherMails", [None])[0] mail = info.get("mail", None) or info.get("otherMails", [None])[0]
# Format group info # Format group info

View File

@ -2,8 +2,8 @@
from typing import Any from typing import Any
from authentik.sources.oauth.models import AuthorizationCodeAuthMethod
from authentik.sources.oauth.types.registry import SourceType, registry from authentik.sources.oauth.types.registry import SourceType, registry
from authentik.sources.oauth.views.callback import OAuthCallback
from authentik.sources.oauth.views.redirect import OAuthRedirect from authentik.sources.oauth.views.redirect import OAuthRedirect
@ -16,15 +16,10 @@ class FacebookOAuthRedirect(OAuthRedirect):
} }
class FacebookOAuth2Callback(OAuthCallback):
"""Facebook OAuth2 Callback"""
@registry.register() @registry.register()
class FacebookType(SourceType): class FacebookType(SourceType):
"""Facebook Type definition""" """Facebook Type definition"""
callback_view = FacebookOAuth2Callback
redirect_view = FacebookOAuthRedirect redirect_view = FacebookOAuthRedirect
verbose_name = "Facebook" verbose_name = "Facebook"
name = "facebook" name = "facebook"
@ -33,6 +28,8 @@ class FacebookType(SourceType):
access_token_url = "https://graph.facebook.com/v7.0/oauth/access_token" # nosec access_token_url = "https://graph.facebook.com/v7.0/oauth/access_token" # nosec
profile_url = "https://graph.facebook.com/v7.0/me?fields=id,name,email" profile_url = "https://graph.facebook.com/v7.0/me?fields=id,name,email"
authorization_code_auth_method = AuthorizationCodeAuthMethod.POST_BODY
def get_base_user_properties(self, info: dict[str, Any], **kwargs) -> dict[str, Any]: def get_base_user_properties(self, info: dict[str, Any], **kwargs) -> dict[str, Any]:
return { return {
"username": info.get("name"), "username": info.get("name"),

View File

@ -5,7 +5,7 @@ from typing import Any
from requests.exceptions import RequestException from requests.exceptions import RequestException
from authentik.sources.oauth.clients.oauth2 import OAuth2Client from authentik.sources.oauth.clients.oauth2 import OAuth2Client
from authentik.sources.oauth.models import OAuthSource from authentik.sources.oauth.models import AuthorizationCodeAuthMethod, OAuthSource
from authentik.sources.oauth.types.registry import SourceType, registry from authentik.sources.oauth.types.registry import SourceType, registry
from authentik.sources.oauth.views.callback import OAuthCallback from authentik.sources.oauth.views.callback import OAuthCallback
from authentik.sources.oauth.views.redirect import OAuthRedirect from authentik.sources.oauth.views.redirect import OAuthRedirect
@ -63,6 +63,8 @@ class GitHubType(SourceType):
) )
oidc_jwks_url = "https://token.actions.githubusercontent.com/.well-known/jwks" oidc_jwks_url = "https://token.actions.githubusercontent.com/.well-known/jwks"
authorization_code_auth_method = AuthorizationCodeAuthMethod.POST_BODY
def get_base_user_properties( def get_base_user_properties(
self, self,
source: OAuthSource, source: OAuthSource,

View File

@ -7,9 +7,8 @@ and https://docs.gitlab.com/ee/integration/openid_connect_provider.html
from typing import Any from typing import Any
from authentik.sources.oauth.models import OAuthSource from authentik.sources.oauth.models import AuthorizationCodeAuthMethod, OAuthSource
from authentik.sources.oauth.types.registry import SourceType, registry from authentik.sources.oauth.types.registry import SourceType, registry
from authentik.sources.oauth.views.callback import OAuthCallback
from authentik.sources.oauth.views.redirect import OAuthRedirect from authentik.sources.oauth.views.redirect import OAuthRedirect
@ -22,15 +21,10 @@ class GitLabOAuthRedirect(OAuthRedirect):
} }
class GitLabOAuthCallback(OAuthCallback):
"""GitLab OAuth2 Callback"""
@registry.register() @registry.register()
class GitLabType(SourceType): class GitLabType(SourceType):
"""GitLab Type definition""" """GitLab Type definition"""
callback_view = GitLabOAuthCallback
redirect_view = GitLabOAuthRedirect redirect_view = GitLabOAuthRedirect
verbose_name = "GitLab" verbose_name = "GitLab"
name = "gitlab" name = "gitlab"
@ -43,6 +37,8 @@ class GitLabType(SourceType):
oidc_well_known_url = "https://gitlab.com/.well-known/openid-configuration" oidc_well_known_url = "https://gitlab.com/.well-known/openid-configuration"
oidc_jwks_url = "https://gitlab.com/oauth/discovery/keys" oidc_jwks_url = "https://gitlab.com/oauth/discovery/keys"
authorization_code_auth_method = AuthorizationCodeAuthMethod.POST_BODY
def get_base_user_properties(self, info: dict[str, Any], **kwargs) -> dict[str, Any]: def get_base_user_properties(self, info: dict[str, Any], **kwargs) -> dict[str, Any]:
return { return {
"username": info.get("preferred_username"), "username": info.get("preferred_username"),

View File

@ -2,8 +2,8 @@
from typing import Any from typing import Any
from authentik.sources.oauth.models import AuthorizationCodeAuthMethod
from authentik.sources.oauth.types.registry import SourceType, registry from authentik.sources.oauth.types.registry import SourceType, registry
from authentik.sources.oauth.views.callback import OAuthCallback
from authentik.sources.oauth.views.redirect import OAuthRedirect from authentik.sources.oauth.views.redirect import OAuthRedirect
@ -16,15 +16,10 @@ class GoogleOAuthRedirect(OAuthRedirect):
} }
class GoogleOAuth2Callback(OAuthCallback):
"""Google OAuth2 Callback"""
@registry.register() @registry.register()
class GoogleType(SourceType): class GoogleType(SourceType):
"""Google Type definition""" """Google Type definition"""
callback_view = GoogleOAuth2Callback
redirect_view = GoogleOAuthRedirect redirect_view = GoogleOAuthRedirect
verbose_name = "Google" verbose_name = "Google"
name = "google" name = "google"
@ -35,6 +30,8 @@ class GoogleType(SourceType):
oidc_well_known_url = "https://accounts.google.com/.well-known/openid-configuration" oidc_well_known_url = "https://accounts.google.com/.well-known/openid-configuration"
oidc_jwks_url = "https://www.googleapis.com/oauth2/v3/certs" oidc_jwks_url = "https://www.googleapis.com/oauth2/v3/certs"
authorization_code_auth_method = AuthorizationCodeAuthMethod.POST_BODY
def get_base_user_properties(self, info: dict[str, Any], **kwargs) -> dict[str, Any]: def get_base_user_properties(self, info: dict[str, Any], **kwargs) -> dict[str, Any]:
return { return {
"email": info.get("email"), "email": info.get("email"),

View File

@ -6,6 +6,7 @@ from requests.exceptions import RequestException
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from authentik.sources.oauth.clients.oauth2 import OAuth2Client from authentik.sources.oauth.clients.oauth2 import OAuth2Client
from authentik.sources.oauth.models import AuthorizationCodeAuthMethod
from authentik.sources.oauth.types.registry import SourceType, registry from authentik.sources.oauth.types.registry import SourceType, registry
from authentik.sources.oauth.views.callback import OAuthCallback from authentik.sources.oauth.views.callback import OAuthCallback
from authentik.sources.oauth.views.redirect import OAuthRedirect from authentik.sources.oauth.views.redirect import OAuthRedirect
@ -59,6 +60,8 @@ class MailcowType(SourceType):
urls_customizable = True urls_customizable = True
authorization_code_auth_method = AuthorizationCodeAuthMethod.POST_BODY
def get_base_user_properties(self, info: dict[str, Any], **kwargs) -> dict[str, Any]: def get_base_user_properties(self, info: dict[str, Any], **kwargs) -> dict[str, Any]:
return { return {
"username": info.get("full_name"), "username": info.get("full_name"),

View File

@ -2,8 +2,10 @@
from typing import Any from typing import Any
from requests.auth import AuthBase, HTTPBasicAuth
from authentik.sources.oauth.clients.oauth2 import UserprofileHeaderAuthClient from authentik.sources.oauth.clients.oauth2 import UserprofileHeaderAuthClient
from authentik.sources.oauth.models import OAuthSource from authentik.sources.oauth.models import AuthorizationCodeAuthMethod, OAuthSource
from authentik.sources.oauth.types.registry import SourceType, registry from authentik.sources.oauth.types.registry import SourceType, registry
from authentik.sources.oauth.views.callback import OAuthCallback from authentik.sources.oauth.views.callback import OAuthCallback
from authentik.sources.oauth.views.redirect import OAuthRedirect from authentik.sources.oauth.views.redirect import OAuthRedirect
@ -18,10 +20,27 @@ class OpenIDConnectOAuthRedirect(OAuthRedirect):
} }
class OpenIDConnectClient(UserprofileHeaderAuthClient):
def get_access_token_args(self, callback: str, code: str) -> dict[str, Any]:
args = super().get_access_token_args(callback, code)
if self.source.authorization_code_auth_method == AuthorizationCodeAuthMethod.POST_BODY:
args["client_id"] = self.get_client_id()
args["client_secret"] = self.get_client_secret()
else:
args.pop("client_id", None)
args.pop("client_secret", None)
return args
def get_access_token_auth(self) -> AuthBase | None:
if self.source.authorization_code_auth_method == AuthorizationCodeAuthMethod.BASIC_AUTH:
return HTTPBasicAuth(self.get_client_id(), self.get_client_secret())
return None
class OpenIDConnectOAuth2Callback(OAuthCallback): class OpenIDConnectOAuth2Callback(OAuthCallback):
"""OpenIDConnect OAuth2 Callback""" """OpenIDConnect OAuth2 Callback"""
client_class = UserprofileHeaderAuthClient client_class = OpenIDConnectClient
def get_user_id(self, info: dict[str, str]) -> str: def get_user_id(self, info: dict[str, str]) -> str:
return info.get("sub", None) return info.get("sub", None)

View File

@ -2,7 +2,6 @@
from typing import Any from typing import Any
from authentik.sources.oauth.clients.oauth2 import UserprofileHeaderAuthClient
from authentik.sources.oauth.models import OAuthSource from authentik.sources.oauth.models import OAuthSource
from authentik.sources.oauth.types.oidc import OpenIDConnectOAuth2Callback from authentik.sources.oauth.types.oidc import OpenIDConnectOAuth2Callback
from authentik.sources.oauth.types.registry import SourceType, registry from authentik.sources.oauth.types.registry import SourceType, registry
@ -18,20 +17,11 @@ class OktaOAuthRedirect(OAuthRedirect):
} }
class OktaOAuth2Callback(OpenIDConnectOAuth2Callback):
"""Okta OAuth2 Callback"""
# Okta has the same quirk as azure and throws an error if the access token
# is set via query parameter, so we reuse the azure client
# see https://github.com/goauthentik/authentik/issues/1910
client_class = UserprofileHeaderAuthClient
@registry.register() @registry.register()
class OktaType(SourceType): class OktaType(SourceType):
"""Okta Type definition""" """Okta Type definition"""
callback_view = OktaOAuth2Callback callback_view = OpenIDConnectOAuth2Callback
redirect_view = OktaOAuthRedirect redirect_view = OktaOAuthRedirect
verbose_name = "Okta" verbose_name = "Okta"
name = "okta" name = "okta"

View File

@ -3,7 +3,7 @@
from typing import Any from typing import Any
from authentik.sources.oauth.clients.oauth2 import UserprofileHeaderAuthClient from authentik.sources.oauth.clients.oauth2 import UserprofileHeaderAuthClient
from authentik.sources.oauth.models import OAuthSource from authentik.sources.oauth.models import AuthorizationCodeAuthMethod, OAuthSource
from authentik.sources.oauth.types.registry import SourceType, registry from authentik.sources.oauth.types.registry import SourceType, registry
from authentik.sources.oauth.views.callback import OAuthCallback from authentik.sources.oauth.views.callback import OAuthCallback
from authentik.sources.oauth.views.redirect import OAuthRedirect from authentik.sources.oauth.views.redirect import OAuthRedirect
@ -41,6 +41,8 @@ class PatreonType(SourceType):
access_token_url = "https://www.patreon.com/api/oauth2/token" # nosec access_token_url = "https://www.patreon.com/api/oauth2/token" # nosec
profile_url = "https://www.patreon.com/api/oauth2/api/current_user" profile_url = "https://www.patreon.com/api/oauth2/api/current_user"
authorization_code_auth_method = AuthorizationCodeAuthMethod.POST_BODY
def get_base_user_properties(self, info: dict[str, Any], **kwargs) -> dict[str, Any]: def get_base_user_properties(self, info: dict[str, Any], **kwargs) -> dict[str, Any]:
return { return {
"username": info.get("data", {}).get("attributes", {}).get("vanity"), "username": info.get("data", {}).get("attributes", {}).get("vanity"),

View File

@ -2,8 +2,6 @@
from typing import Any from typing import Any
from requests.auth import HTTPBasicAuth
from authentik.sources.oauth.clients.oauth2 import UserprofileHeaderAuthClient from authentik.sources.oauth.clients.oauth2 import UserprofileHeaderAuthClient
from authentik.sources.oauth.types.registry import SourceType, registry from authentik.sources.oauth.types.registry import SourceType, registry
from authentik.sources.oauth.views.callback import OAuthCallback from authentik.sources.oauth.views.callback import OAuthCallback
@ -20,21 +18,10 @@ class RedditOAuthRedirect(OAuthRedirect):
} }
class RedditOAuth2Client(UserprofileHeaderAuthClient):
"""Reddit OAuth2 Client"""
def get_access_token(self, **request_kwargs):
"Fetch access token from callback request."
request_kwargs["auth"] = HTTPBasicAuth(
self.source.consumer_key, self.source.consumer_secret
)
return super().get_access_token(**request_kwargs)
class RedditOAuth2Callback(OAuthCallback): class RedditOAuth2Callback(OAuthCallback):
"""Reddit OAuth2 Callback""" """Reddit OAuth2 Callback"""
client_class = RedditOAuth2Client client_class = UserprofileHeaderAuthClient
@registry.register() @registry.register()

View File

@ -10,7 +10,7 @@ from django.urls.base import reverse
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from authentik.flows.challenge import Challenge, RedirectChallenge from authentik.flows.challenge import Challenge, RedirectChallenge
from authentik.sources.oauth.models import OAuthSource from authentik.sources.oauth.models import AuthorizationCodeAuthMethod, OAuthSource
from authentik.sources.oauth.views.callback import OAuthCallback from authentik.sources.oauth.views.callback import OAuthCallback
from authentik.sources.oauth.views.redirect import OAuthRedirect from authentik.sources.oauth.views.redirect import OAuthRedirect
@ -41,6 +41,10 @@ class SourceType:
oidc_well_known_url: str | None = None oidc_well_known_url: str | None = None
oidc_jwks_url: str | None = None oidc_jwks_url: str | None = None
authorization_code_auth_method: AuthorizationCodeAuthMethod = (
AuthorizationCodeAuthMethod.BASIC_AUTH
)
def icon_url(self) -> str: def icon_url(self) -> str:
"""Get Icon URL for login""" """Get Icon URL for login"""
return static(f"authentik/sources/{self.name}.svg") return static(f"authentik/sources/{self.name}.svg")

View File

@ -4,6 +4,7 @@ from json import dumps
from typing import Any from typing import Any
from authentik.sources.oauth.clients.oauth2 import UserprofileHeaderAuthClient from authentik.sources.oauth.clients.oauth2 import UserprofileHeaderAuthClient
from authentik.sources.oauth.models import AuthorizationCodeAuthMethod
from authentik.sources.oauth.types.oidc import OpenIDConnectOAuth2Callback from authentik.sources.oauth.types.oidc import OpenIDConnectOAuth2Callback
from authentik.sources.oauth.types.registry import SourceType, registry from authentik.sources.oauth.types.registry import SourceType, registry
from authentik.sources.oauth.views.redirect import OAuthRedirect from authentik.sources.oauth.views.redirect import OAuthRedirect
@ -47,6 +48,8 @@ class TwitchType(SourceType):
access_token_url = "https://id.twitch.tv/oauth2/token" # nosec access_token_url = "https://id.twitch.tv/oauth2/token" # nosec
profile_url = "https://id.twitch.tv/oauth2/userinfo" profile_url = "https://id.twitch.tv/oauth2/userinfo"
authorization_code_auth_method = AuthorizationCodeAuthMethod.POST_BODY
def get_base_user_properties(self, info: dict[str, Any], **kwargs) -> dict[str, Any]: def get_base_user_properties(self, info: dict[str, Any], **kwargs) -> dict[str, Any]:
return { return {
"username": info.get("preferred_username"), "username": info.get("preferred_username"),

View File

@ -12,23 +12,6 @@ from authentik.sources.oauth.views.callback import OAuthCallback
from authentik.sources.oauth.views.redirect import OAuthRedirect from authentik.sources.oauth.views.redirect import OAuthRedirect
class TwitterClient(UserprofileHeaderAuthClient):
"""Twitter has similar quirks to Azure AD, and additionally requires Basic auth on
the access token endpoint for some reason."""
# Twitter has the same quirk as azure and throws an error if the access token
# is set via query parameter, so we reuse the azure client
# see https://github.com/goauthentik/authentik/issues/1910
def get_access_token(self, **request_kwargs) -> dict[str, Any] | None:
return super().get_access_token(
auth=(
self.source.consumer_key,
self.source.consumer_secret,
)
)
class TwitterOAuthRedirect(OAuthRedirect): class TwitterOAuthRedirect(OAuthRedirect):
"""Twitter OAuth2 Redirect""" """Twitter OAuth2 Redirect"""
@ -44,7 +27,7 @@ class TwitterOAuthRedirect(OAuthRedirect):
class TwitterOAuthCallback(OAuthCallback): class TwitterOAuthCallback(OAuthCallback):
"""Twitter OAuth2 Callback""" """Twitter OAuth2 Callback"""
client_class = TwitterClient client_class = UserprofileHeaderAuthClient
def get_user_id(self, info: dict[str, str]) -> str: def get_user_id(self, info: dict[str, str]) -> str:
return info.get("data", {}).get("id", "") return info.get("data", {}).get("id", "")

File diff suppressed because one or more lines are too long

View File

@ -2,4 +2,4 @@
from authentik.stages.dummy.api import DummyStageViewSet from authentik.stages.dummy.api import DummyStageViewSet
api_urlpatterns = [("stages/dummy", DummyStageViewSet)] api_urlpatterns = [("stages/dummy", DummyStageViewSet, "stages-dummy")]

View File

@ -4,11 +4,12 @@ from drf_spectacular.utils import extend_schema
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.serializers import CharField, ModelSerializer from rest_framework.serializers import CharField
from rest_framework.validators import UniqueValidator from rest_framework.validators import UniqueValidator
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from authentik.core.api.used_by import UsedByMixin from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer
from authentik.core.expression.exceptions import PropertyMappingExpressionException from authentik.core.expression.exceptions import PropertyMappingExpressionException
from authentik.flows.api.stages import StageSerializer from authentik.flows.api.stages import StageSerializer
from authentik.flows.challenge import HttpChallengeResponse from authentik.flows.challenge import HttpChallengeResponse

View File

@ -15,12 +15,12 @@ from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.permissions import BasePermission from rest_framework.permissions import BasePermission
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.serializers import DateTimeField, ModelSerializer from rest_framework.serializers import DateTimeField
from rest_framework.views import View from rest_framework.views import View
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from authentik.api.authentication import validate_auth from authentik.api.authentication import validate_auth
from authentik.core.api.utils import PassiveSerializer from authentik.core.api.utils import ModelSerializer, PassiveSerializer
from authentik.core.models import User from authentik.core.models import User
from authentik.lib.config import CONFIG from authentik.lib.config import CONFIG
from authentik.recovery.lib import create_admin_group, create_recovery_token from authentik.recovery.lib import create_admin_group, create_recovery_token

View File

@ -1201,6 +1201,46 @@
} }
} }
}, },
{
"type": "object",
"required": [
"model",
"identifiers"
],
"properties": {
"model": {
"const": "authentik_rbac.initialpermissions"
},
"id": {
"type": "string"
},
"state": {
"type": "string",
"enum": [
"absent",
"present",
"created",
"must_created"
],
"default": "present"
},
"conditions": {
"type": "array",
"items": {
"type": "boolean"
}
},
"permissions": {
"$ref": "#/$defs/model_authentik_rbac.initialpermissions_permissions"
},
"attrs": {
"$ref": "#/$defs/model_authentik_rbac.initialpermissions"
},
"identifiers": {
"$ref": "#/$defs/model_authentik_rbac.initialpermissions"
}
}
},
{ {
"type": "object", "type": "object",
"required": [ "required": [
@ -4828,6 +4868,7 @@
"authentik_providers_scim.scimprovider", "authentik_providers_scim.scimprovider",
"authentik_providers_scim.scimmapping", "authentik_providers_scim.scimmapping",
"authentik_rbac.role", "authentik_rbac.role",
"authentik_rbac.initialpermissions",
"authentik_sources_kerberos.kerberossource", "authentik_sources_kerberos.kerberossource",
"authentik_sources_kerberos.kerberossourcepropertymapping", "authentik_sources_kerberos.kerberossourcepropertymapping",
"authentik_sources_kerberos.userkerberossourceconnection", "authentik_sources_kerberos.userkerberossourceconnection",
@ -7169,12 +7210,16 @@
"authentik_providers_ssf.view_stream", "authentik_providers_ssf.view_stream",
"authentik_providers_ssf.view_streamevent", "authentik_providers_ssf.view_streamevent",
"authentik_rbac.access_admin_interface", "authentik_rbac.access_admin_interface",
"authentik_rbac.add_initialpermissions",
"authentik_rbac.add_role", "authentik_rbac.add_role",
"authentik_rbac.assign_role_permissions", "authentik_rbac.assign_role_permissions",
"authentik_rbac.change_initialpermissions",
"authentik_rbac.change_role", "authentik_rbac.change_role",
"authentik_rbac.delete_initialpermissions",
"authentik_rbac.delete_role", "authentik_rbac.delete_role",
"authentik_rbac.edit_system_settings", "authentik_rbac.edit_system_settings",
"authentik_rbac.unassign_role_permissions", "authentik_rbac.unassign_role_permissions",
"authentik_rbac.view_initialpermissions",
"authentik_rbac.view_role", "authentik_rbac.view_role",
"authentik_rbac.view_system_info", "authentik_rbac.view_system_info",
"authentik_rbac.view_system_settings", "authentik_rbac.view_system_settings",
@ -7461,6 +7506,64 @@
} }
} }
}, },
"model_authentik_rbac.initialpermissions": {
"type": "object",
"properties": {
"name": {
"type": "string",
"maxLength": 150,
"minLength": 1,
"title": "Name"
},
"mode": {
"type": "string",
"enum": [
"user",
"role"
],
"title": "Mode"
},
"role": {
"type": "string",
"format": "uuid",
"title": "Role"
},
"permissions": {
"type": "array",
"items": {
"type": "integer"
},
"title": "Permissions"
}
},
"required": []
},
"model_authentik_rbac.initialpermissions_permissions": {
"type": "array",
"items": {
"type": "object",
"required": [
"permission"
],
"properties": {
"permission": {
"type": "string",
"enum": [
"add_initialpermissions",
"change_initialpermissions",
"delete_initialpermissions",
"view_initialpermissions"
]
},
"user": {
"type": "integer"
},
"role": {
"type": "string"
}
}
}
},
"model_authentik_sources_kerberos.kerberossource": { "model_authentik_sources_kerberos.kerberossource": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -8333,6 +8436,15 @@
"type": "object", "type": "object",
"additionalProperties": true, "additionalProperties": true,
"title": "Oidc jwks" "title": "Oidc jwks"
},
"authorization_code_auth_method": {
"type": "string",
"enum": [
"basic_auth",
"post_body"
],
"title": "Authorization code auth method",
"description": "How to perform authentication during an authorization_code token request flow"
} }
}, },
"required": [] "required": []
@ -13793,12 +13905,16 @@
"authentik_providers_ssf.view_stream", "authentik_providers_ssf.view_stream",
"authentik_providers_ssf.view_streamevent", "authentik_providers_ssf.view_streamevent",
"authentik_rbac.access_admin_interface", "authentik_rbac.access_admin_interface",
"authentik_rbac.add_initialpermissions",
"authentik_rbac.add_role", "authentik_rbac.add_role",
"authentik_rbac.assign_role_permissions", "authentik_rbac.assign_role_permissions",
"authentik_rbac.change_initialpermissions",
"authentik_rbac.change_role", "authentik_rbac.change_role",
"authentik_rbac.delete_initialpermissions",
"authentik_rbac.delete_role", "authentik_rbac.delete_role",
"authentik_rbac.edit_system_settings", "authentik_rbac.edit_system_settings",
"authentik_rbac.unassign_role_permissions", "authentik_rbac.unassign_role_permissions",
"authentik_rbac.view_initialpermissions",
"authentik_rbac.view_role", "authentik_rbac.view_role",
"authentik_rbac.view_system_info", "authentik_rbac.view_system_info",
"authentik_rbac.view_system_settings", "authentik_rbac.view_system_settings",

10
eslint.config.mjs Normal file
View File

@ -0,0 +1,10 @@
import { createESLintPackageConfig } from "@goauthentik/eslint-config";
// @ts-check
/**
* ESLint configuration for authentik's monorepo.
*/
const ESLintConfig = createESLintPackageConfig();
export default ESLintConfig;

2
go.mod
View File

@ -27,7 +27,7 @@ require (
github.com/spf13/cobra v1.9.1 github.com/spf13/cobra v1.9.1
github.com/stretchr/testify v1.10.0 github.com/stretchr/testify v1.10.0
github.com/wwt/guac v1.3.2 github.com/wwt/guac v1.3.2
goauthentik.io/api/v3 v3.2025024.4 goauthentik.io/api/v3 v3.2025024.6
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
golang.org/x/oauth2 v0.29.0 golang.org/x/oauth2 v0.29.0
golang.org/x/sync v0.13.0 golang.org/x/sync v0.13.0

4
go.sum
View File

@ -300,8 +300,8 @@ go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
goauthentik.io/api/v3 v3.2025024.4 h1:fD4K6YcCTdwtkqKbYBdJk3POHVzw+LDdJdZSbOAKbX4= goauthentik.io/api/v3 v3.2025024.6 h1:3mmZY7E0EM/RR8uMF17mxa7368ZgZEIq/FjlCLJ9+lA=
goauthentik.io/api/v3 v3.2025024.4/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw= goauthentik.io/api/v3 v3.2025024.6/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=

View File

@ -1,15 +1,34 @@
<!DOCTYPE html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
<title>{{.Title}}</title> <title>{{.Title}}</title>
<link rel="shortcut icon" type="image/png" href="/outpost.goauthentik.io/static/dist/assets/icons/icon.png">
<link rel="stylesheet" type="text/css" href="/outpost.goauthentik.io/static/dist/patternfly.min.css"> <link
<link rel="stylesheet" type="text/css" href="/outpost.goauthentik.io/static/dist/authentik.css"> rel="shortcut icon"
<link rel="prefetch" href="/outpost.goauthentik.io/static/dist/assets/images/flow_background.jpg" /> type="image/png"
<style> href="/outpost.goauthentik.io/static/dist/assets/icons/icon.png"
/>
<link
rel="stylesheet"
type="text/css"
href="/outpost.goauthentik.io/static/dist/patternfly.min.css"
/>
<link
rel="stylesheet"
type="text/css"
href="/outpost.goauthentik.io/static/dist/authentik.css"
/>
<link
rel="prefetch"
href="/outpost.goauthentik.io/static/dist/assets/images/flow_background.jpg"
/>
<style data-test-id="outpost-error-root-styles">
.pf-c-background-image::before { .pf-c-background-image::before {
--ak-flow-background: url("/outpost.goauthentik.io/static/dist/assets/images/flow_background.jpg"); --ak-flow-background: url("/outpost.goauthentik.io/static/dist/assets/images/flow_background.jpg");
} }
@ -24,13 +43,15 @@
</style> </style>
</head> </head>
<body> <body>
<div class="pf-c-background-image"> <div class="pf-c-background-image"></div>
</div>
<div class="pf-c-login stacked"> <div class="pf-c-login stacked">
<div class="ak-login-container"> <div class="ak-login-container">
<main class="pf-c-login__main"> <main class="pf-c-login__main">
<div class="pf-c-login__main-header pf-c-brand ak-brand"> <div class="pf-c-login__main-header pf-c-brand ak-brand">
<img src="/outpost.goauthentik.io/static/dist/assets/icons/icon_left_brand.svg" alt="authentik Logo" /> <img
src="/outpost.goauthentik.io/static/dist/assets/icons/icon_left_brand.svg"
alt="authentik Logo"
/>
</div> </div>
<header class="pf-c-login__main-header"> <header class="pf-c-login__main-header">
<h1 class="pf-c-title pf-m-3xl"> <h1 class="pf-c-title pf-m-3xl">
@ -47,9 +68,7 @@
<footer class="pf-c-login__footer"> <footer class="pf-c-login__footer">
<ul class="pf-c-list pf-m-inline"> <ul class="pf-c-list pf-m-inline">
<li> <li>
<span> <span> Powered by authentik </span>
Powered by authentik
</span>
</li> </li>
</ul> </ul>
</footer> </footer>

View File

@ -1,16 +1,16 @@
{ {
"name": "@goauthentik/lifecycle-aws", "name": "@goauthentik/lifecycle-aws",
"version": "0.0.0", "version": "0.0.0",
"private": true,
"license": "MIT", "license": "MIT",
"private": true,
"scripts": { "scripts": {
"aws-cfn": "cross-env CI=false cdk synth --version-reporting=false > template.yaml" "aws-cfn": "cross-env CI=false cdk synth --version-reporting=false > template.yaml"
}, },
"engines": {
"node": ">=20"
},
"devDependencies": { "devDependencies": {
"aws-cdk": "^2.1007.0", "aws-cdk": "^2.1007.0",
"cross-env": "^7.0.3" "cross-env": "^7.0.3"
},
"engines": {
"node": ">=20.11"
} }
} }

View File

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-04-14 00:11+0000\n" "POT-Creation-Date: 2025-04-15 00:11+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -169,6 +169,7 @@ msgid "User's display name."
msgstr "" msgstr ""
#: authentik/core/models.py authentik/providers/oauth2/models.py #: authentik/core/models.py authentik/providers/oauth2/models.py
#: authentik/rbac/models.py
msgid "User" msgid "User"
msgstr "" msgstr ""
@ -1984,6 +1985,10 @@ msgstr ""
msgid "Roles" msgid "Roles"
msgstr "" msgstr ""
#: authentik/rbac/models.py
msgid "Initial Permissions"
msgstr ""
#: authentik/rbac/models.py #: authentik/rbac/models.py
msgid "System permission" msgid "System permission"
msgstr "" msgstr ""
@ -3468,6 +3473,14 @@ msgid ""
"seconds=2)." "seconds=2)."
msgstr "" msgstr ""
#: authentik/tenants/models.py
msgid "Reputation cannot decrease lower than this value. Zero or negative."
msgstr ""
#: authentik/tenants/models.py
msgid "Reputation cannot increase higher than this value. Zero or positive."
msgstr ""
#: authentik/tenants/models.py #: authentik/tenants/models.py
msgid "The option configures the footer links on the flow executor pages." msgid "The option configures the footer links on the flow executor pages."
msgstr "" msgstr ""

View File

@ -9,9 +9,9 @@
# Kyllian Delaye-Maillot, 2023 # Kyllian Delaye-Maillot, 2023
# Manuel Viens, 2023 # Manuel Viens, 2023
# Mordecai, 2023 # Mordecai, 2023
# nerdinator <florian.dupret@gmail.com>, 2024
# Charles Leclerc, 2025 # Charles Leclerc, 2025
# Tina, 2025 # Tina, 2025
# nerdinator <florian.dupret@gmail.com>, 2025
# Marc Schmitt, 2025 # Marc Schmitt, 2025
# #
#, fuzzy #, fuzzy
@ -19,7 +19,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-04-11 00:10+0000\n" "POT-Creation-Date: 2025-04-15 00:11+0000\n"
"PO-Revision-Date: 2022-09-26 16:47+0000\n" "PO-Revision-Date: 2022-09-26 16:47+0000\n"
"Last-Translator: Marc Schmitt, 2025\n" "Last-Translator: Marc Schmitt, 2025\n"
"Language-Team: French (https://app.transifex.com/authentik/teams/119923/fr/)\n" "Language-Team: French (https://app.transifex.com/authentik/teams/119923/fr/)\n"
@ -194,6 +194,7 @@ msgid "User's display name."
msgstr "Nom d'affichage de l'utilisateur" msgstr "Nom d'affichage de l'utilisateur"
#: authentik/core/models.py authentik/providers/oauth2/models.py #: authentik/core/models.py authentik/providers/oauth2/models.py
#: authentik/rbac/models.py
msgid "User" msgid "User"
msgstr "Utilisateur" msgstr "Utilisateur"
@ -382,6 +383,18 @@ msgstr "Mappage de propriété"
msgid "Property Mappings" msgid "Property Mappings"
msgstr "Mappages de propriété" msgstr "Mappages de propriété"
#: authentik/core/models.py
msgid "session data"
msgstr "Données de session"
#: authentik/core/models.py
msgid "Session"
msgstr "Session"
#: authentik/core/models.py
msgid "Sessions"
msgstr "Sessions"
#: authentik/core/models.py #: authentik/core/models.py
msgid "Authenticated Session" msgid "Authenticated Session"
msgstr "Session Authentifiée" msgstr "Session Authentifiée"
@ -2197,6 +2210,10 @@ msgstr "Rôle"
msgid "Roles" msgid "Roles"
msgstr "Rôles" msgstr "Rôles"
#: authentik/rbac/models.py
msgid "Initial Permissions"
msgstr "Permissions initiales"
#: authentik/rbac/models.py #: authentik/rbac/models.py
msgid "System permission" msgid "System permission"
msgstr "Permission système" msgstr "Permission système"
@ -2447,6 +2464,9 @@ msgid ""
"attribute. This allows nested group resolution on systems like FreeIPA and " "attribute. This allows nested group resolution on systems like FreeIPA and "
"Active Directory" "Active Directory"
msgstr "" msgstr ""
"Recherche de l'appartenance aux groupes basée sur un attribut utilisateur "
"plutôt que sur un attribut de groupe. Cela permet la résolution des groupes "
"imbriqués sur des systèmes tels que FreeIPA et Active Directory."
#: authentik/sources/ldap/models.py #: authentik/sources/ldap/models.py
msgid "LDAP Source" msgid "LDAP Source"
@ -2464,6 +2484,22 @@ msgstr "Mappage de propriété source LDAP"
msgid "LDAP Source Property Mappings" msgid "LDAP Source Property Mappings"
msgstr "Mappages de propriété source LDAP" msgstr "Mappages de propriété source LDAP"
#: authentik/sources/ldap/models.py
msgid "User LDAP Source Connection"
msgstr "Connexion de l'utilisateur à la source LDAP"
#: authentik/sources/ldap/models.py
msgid "User LDAP Source Connections"
msgstr "Connexions de l'utilisateur à la source LDAP"
#: authentik/sources/ldap/models.py
msgid "Group LDAP Source Connection"
msgstr "Connexion du groupe à la source LDAP"
#: authentik/sources/ldap/models.py
msgid "Group LDAP Source Connections"
msgstr "Connexions du groupe à la source LDAP"
#: authentik/sources/ldap/signals.py #: authentik/sources/ldap/signals.py
msgid "Password does not match Active Directory Complexity." msgid "Password does not match Active Directory Complexity."
msgstr "Le mot de passe ne correspond pas à la complexité d'Active Directory." msgstr "Le mot de passe ne correspond pas à la complexité d'Active Directory."
@ -3817,6 +3853,17 @@ msgstr ""
"Les évènements seront supprimés après cet interval. (Format : " "Les évènements seront supprimés après cet interval. (Format : "
"weeks=3;days=2;hours=3,seconds=2)" "weeks=3;days=2;hours=3,seconds=2)"
#: authentik/tenants/models.py
msgid "Reputation cannot decrease lower than this value. Zero or negative."
msgstr ""
"La réputation ne peut pas descendre en dessous de cette valeur. Zéro ou "
"négatif."
#: authentik/tenants/models.py
msgid "Reputation cannot increase higher than this value. Zero or positive."
msgstr ""
"La réputation ne peut pas monter au dessus de cette valeur. Zéro ou positif."
#: authentik/tenants/models.py #: authentik/tenants/models.py
msgid "The option configures the footer links on the flow executor pages." msgid "The option configures the footer links on the flow executor pages."
msgstr "" msgstr ""

Binary file not shown.

Binary file not shown.

View File

@ -15,7 +15,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-04-11 00:10+0000\n" "POT-Creation-Date: 2025-04-15 00:11+0000\n"
"PO-Revision-Date: 2022-09-26 16:47+0000\n" "PO-Revision-Date: 2022-09-26 16:47+0000\n"
"Last-Translator: deluxghost, 2025\n" "Last-Translator: deluxghost, 2025\n"
"Language-Team: Chinese Simplified (https://app.transifex.com/authentik/teams/119923/zh-Hans/)\n" "Language-Team: Chinese Simplified (https://app.transifex.com/authentik/teams/119923/zh-Hans/)\n"
@ -179,6 +179,7 @@ msgid "User's display name."
msgstr "用户的显示名称。" msgstr "用户的显示名称。"
#: authentik/core/models.py authentik/providers/oauth2/models.py #: authentik/core/models.py authentik/providers/oauth2/models.py
#: authentik/rbac/models.py
msgid "User" msgid "User"
msgstr "用户" msgstr "用户"
@ -345,6 +346,18 @@ msgstr "属性映射"
msgid "Property Mappings" msgid "Property Mappings"
msgstr "属性映射" msgstr "属性映射"
#: authentik/core/models.py
msgid "session data"
msgstr "会话数据"
#: authentik/core/models.py
msgid "Session"
msgstr "会话"
#: authentik/core/models.py
msgid "Sessions"
msgstr "会话"
#: authentik/core/models.py #: authentik/core/models.py
msgid "Authenticated Session" msgid "Authenticated Session"
msgstr "已认证会话" msgstr "已认证会话"
@ -1243,11 +1256,11 @@ msgstr "正在等待身份验证…"
msgid "" msgid ""
"You're already authenticating in another tab. This page will refresh once " "You're already authenticating in another tab. This page will refresh once "
"authentication is completed." "authentication is completed."
msgstr "" msgstr "您正在另一个标签页中验证身份。身份验证完成后,此页面会刷新。"
#: authentik/policies/templates/policies/buffer.html #: authentik/policies/templates/policies/buffer.html
msgid "Authenticate in this tab" msgid "Authenticate in this tab"
msgstr "" msgstr "在此标签页中验证身份"
#: authentik/policies/templates/policies/denied.html #: authentik/policies/templates/policies/denied.html
msgid "Permission denied" msgid "Permission denied"
@ -1999,6 +2012,10 @@ msgstr "角色"
msgid "Roles" msgid "Roles"
msgstr "角色" msgstr "角色"
#: authentik/rbac/models.py
msgid "Initial Permissions"
msgstr "初始权限"
#: authentik/rbac/models.py #: authentik/rbac/models.py
msgid "System permission" msgid "System permission"
msgstr "系统权限" msgstr "系统权限"
@ -2227,7 +2244,7 @@ msgid ""
"Lookup group membership based on a user attribute instead of a group " "Lookup group membership based on a user attribute instead of a group "
"attribute. This allows nested group resolution on systems like FreeIPA and " "attribute. This allows nested group resolution on systems like FreeIPA and "
"Active Directory" "Active Directory"
msgstr "" msgstr "基于用户属性而非组属性查询组成员身份。这允许在 FreeIPA 或 Active Directory 等系统上支持嵌套组决策"
#: authentik/sources/ldap/models.py #: authentik/sources/ldap/models.py
msgid "LDAP Source" msgid "LDAP Source"
@ -2245,6 +2262,22 @@ msgstr "LDAP 源属性映射"
msgid "LDAP Source Property Mappings" msgid "LDAP Source Property Mappings"
msgstr "LDAP 源属性映射" msgstr "LDAP 源属性映射"
#: authentik/sources/ldap/models.py
msgid "User LDAP Source Connection"
msgstr "用户 LDAP 源连接"
#: authentik/sources/ldap/models.py
msgid "User LDAP Source Connections"
msgstr "用户 LDAP 源连接"
#: authentik/sources/ldap/models.py
msgid "Group LDAP Source Connection"
msgstr "组 LDAP 源连接"
#: authentik/sources/ldap/models.py
msgid "Group LDAP Source Connections"
msgstr "组 LDAP 源连接"
#: authentik/sources/ldap/signals.py #: authentik/sources/ldap/signals.py
msgid "Password does not match Active Directory Complexity." msgid "Password does not match Active Directory Complexity."
msgstr "密码与 Active Directory 复杂度不匹配。" msgstr "密码与 Active Directory 复杂度不匹配。"
@ -3505,6 +3538,14 @@ msgid ""
"weeks=3;days=2;hours=3,seconds=2)." "weeks=3;days=2;hours=3,seconds=2)."
msgstr "事件会在多久后被删除。格式weeks=3;days=2;hours=3,seconds=2。" msgstr "事件会在多久后被删除。格式weeks=3;days=2;hours=3,seconds=2。"
#: authentik/tenants/models.py
msgid "Reputation cannot decrease lower than this value. Zero or negative."
msgstr "信誉无法降低到此值以下。可为零或负数。"
#: authentik/tenants/models.py
msgid "Reputation cannot increase higher than this value. Zero or positive."
msgstr "信誉无法提高到此值以上。可为零或正数。"
#: authentik/tenants/models.py #: authentik/tenants/models.py
msgid "The option configures the footer links on the flow executor pages." msgid "The option configures the footer links on the flow executor pages."
msgstr "此选项配置流程执行器页面上的页脚链接。" msgstr "此选项配置流程执行器页面上的页脚链接。"

View File

@ -14,7 +14,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-04-11 00:10+0000\n" "POT-Creation-Date: 2025-04-15 00:11+0000\n"
"PO-Revision-Date: 2022-09-26 16:47+0000\n" "PO-Revision-Date: 2022-09-26 16:47+0000\n"
"Last-Translator: deluxghost, 2025\n" "Last-Translator: deluxghost, 2025\n"
"Language-Team: Chinese (China) (https://app.transifex.com/authentik/teams/119923/zh_CN/)\n" "Language-Team: Chinese (China) (https://app.transifex.com/authentik/teams/119923/zh_CN/)\n"
@ -178,6 +178,7 @@ msgid "User's display name."
msgstr "用户的显示名称。" msgstr "用户的显示名称。"
#: authentik/core/models.py authentik/providers/oauth2/models.py #: authentik/core/models.py authentik/providers/oauth2/models.py
#: authentik/rbac/models.py
msgid "User" msgid "User"
msgstr "用户" msgstr "用户"
@ -344,6 +345,18 @@ msgstr "属性映射"
msgid "Property Mappings" msgid "Property Mappings"
msgstr "属性映射" msgstr "属性映射"
#: authentik/core/models.py
msgid "session data"
msgstr "会话数据"
#: authentik/core/models.py
msgid "Session"
msgstr "会话"
#: authentik/core/models.py
msgid "Sessions"
msgstr "会话"
#: authentik/core/models.py #: authentik/core/models.py
msgid "Authenticated Session" msgid "Authenticated Session"
msgstr "已认证会话" msgstr "已认证会话"
@ -1998,6 +2011,10 @@ msgstr "角色"
msgid "Roles" msgid "Roles"
msgstr "角色" msgstr "角色"
#: authentik/rbac/models.py
msgid "Initial Permissions"
msgstr "初始权限"
#: authentik/rbac/models.py #: authentik/rbac/models.py
msgid "System permission" msgid "System permission"
msgstr "系统权限" msgstr "系统权限"
@ -2226,7 +2243,7 @@ msgid ""
"Lookup group membership based on a user attribute instead of a group " "Lookup group membership based on a user attribute instead of a group "
"attribute. This allows nested group resolution on systems like FreeIPA and " "attribute. This allows nested group resolution on systems like FreeIPA and "
"Active Directory" "Active Directory"
msgstr "" msgstr "基于用户属性而非组属性查询组成员身份。这允许在 FreeIPA 或 Active Directory 等系统上支持嵌套组决策"
#: authentik/sources/ldap/models.py #: authentik/sources/ldap/models.py
msgid "LDAP Source" msgid "LDAP Source"
@ -2244,6 +2261,22 @@ msgstr "LDAP 源属性映射"
msgid "LDAP Source Property Mappings" msgid "LDAP Source Property Mappings"
msgstr "LDAP 源属性映射" msgstr "LDAP 源属性映射"
#: authentik/sources/ldap/models.py
msgid "User LDAP Source Connection"
msgstr "用户 LDAP 源连接"
#: authentik/sources/ldap/models.py
msgid "User LDAP Source Connections"
msgstr "用户 LDAP 源连接"
#: authentik/sources/ldap/models.py
msgid "Group LDAP Source Connection"
msgstr "组 LDAP 源连接"
#: authentik/sources/ldap/models.py
msgid "Group LDAP Source Connections"
msgstr "组 LDAP 源连接"
#: authentik/sources/ldap/signals.py #: authentik/sources/ldap/signals.py
msgid "Password does not match Active Directory Complexity." msgid "Password does not match Active Directory Complexity."
msgstr "密码与 Active Directory 复杂度不匹配。" msgstr "密码与 Active Directory 复杂度不匹配。"
@ -3504,6 +3537,14 @@ msgid ""
"weeks=3;days=2;hours=3,seconds=2)." "weeks=3;days=2;hours=3,seconds=2)."
msgstr "事件会在多久后被删除。格式weeks=3;days=2;hours=3,seconds=2。" msgstr "事件会在多久后被删除。格式weeks=3;days=2;hours=3,seconds=2。"
#: authentik/tenants/models.py
msgid "Reputation cannot decrease lower than this value. Zero or negative."
msgstr "信誉无法降低到此值以下。可为零或负数。"
#: authentik/tenants/models.py
msgid "Reputation cannot increase higher than this value. Zero or positive."
msgstr "信誉无法提高到此值以上。可为零或正数。"
#: authentik/tenants/models.py #: authentik/tenants/models.py
msgid "The option configures the footer links on the flow executor pages." msgid "The option configures the footer links on the flow executor pages."
msgstr "此选项配置流程执行器页面上的页脚链接。" msgstr "此选项配置流程执行器页面上的页脚链接。"

44519
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,50 @@
{ {
"name": "@goauthentik/authentik", "name": "@goauthentik/universe",
"version": "2025.2.4", "version": "2025.2.4",
"private": true "description": "Monorepo for authentik.",
"private": true,
"scripts": {
"lint": "run-s lint:prettier:check lint:eslint:check",
"lint:eslint:check": "eslint .",
"lint:eslint:fix": "eslint --fix .",
"lint:fix": "run-s lint:prettier:fix lint:eslint:fix",
"lint:prettier:check": "prettier --cache --check -u .",
"lint:prettier:fix": "prettier --cache --write -u .",
"typecheck": "NODE_OPTIONS=\"--max-old-space-size=3000\" tsc -b"
},
"dependencies": {
"@eslint/js": "^9.11.1",
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
"@typescript-eslint/eslint-plugin": "^8.28.0",
"@typescript-eslint/parser": "^8.28.0",
"eslint": "^9.23.0",
"eslint-plugin-lit": "^2.0.0",
"eslint-plugin-wc": "^3.0.0",
"npm-run-all": "^4.1.5",
"prettier": "^3.5.3",
"prettier-plugin-django-alpine": "^1.3.0",
"prettier-plugin-packagejson": "^2.5.10",
"typescript": "^5.8.2",
"typescript-eslint": "^8.29.0",
"zx": "^8.4.1"
},
"optionalDependencies": {
"@esbuild/darwin-arm64": "^0.24.0",
"@esbuild/linux-amd64": "^0.18.11",
"@esbuild/linux-arm64": "^0.24.0",
"@rollup/rollup-darwin-arm64": "4.23.0",
"@rollup/rollup-linux-arm64-gnu": "4.23.0",
"@rollup/rollup-linux-x64-gnu": "4.23.0"
},
"engines": {
"node": ">=20.11"
},
"workspaces": [
"gen-ts-api",
"web",
"web/packages/*",
"website",
"packages/*"
],
"prettier": "@goauthentik/prettier-config"
} }

View File

@ -18,9 +18,7 @@
} }
.badge--support-community { .badge--support-community {
--ifm-badge-background-color: var( --ifm-badge-background-color: var(--ifm-color-secondary-contrast-foreground);
--ifm-color-secondary-contrast-foreground
);
--ifm-badge-border-color: var(--ifm-color-secondary-dark); --ifm-badge-border-color: var(--ifm-color-secondary-dark);
--ifm-badge-color: var(--ifm-color-secondary-contrast-background); --ifm-badge-color: var(--ifm-color-secondary-contrast-background);
} }

View File

@ -1,12 +1,12 @@
:root { :root {
--ifm-font-family-base: --ifm-font-family-base:
RedHatVF, system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, RedHatVF, system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans,
Noto Sans, sans-serif, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif,
sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
--ifm-font-family-monospace: --ifm-font-family-monospace:
RedHatMonoVF, SFMono-Regular, Menlo, Monaco, Consolas, RedHatMonoVF, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New",
"Liberation Mono", "Courier New", monospace; monospace;
--ifm-heading-font-family: RedHatDisplayVF, var(--ifm-font-family-base); --ifm-heading-font-family: RedHatDisplayVF, var(--ifm-font-family-base);

View File

@ -7,11 +7,7 @@
} }
.homepage_hero__subtitle p { .homepage_hero__subtitle p {
font-size: clamp( font-size: clamp(1.125rem, 0.9946rem + 0.6522vi, 1.5rem); /* Adjust font as page scales */
1.125rem,
0.9946rem + 0.6522vi,
1.5rem
); /* Adjust font as page scales */
max-width: 28ch; /* Apply a maximum to keep everything in the box */ max-width: 28ch; /* Apply a maximum to keep everything in the box */
text-wrap: balance; /* Prevent widows, orphans, and runts. Doesn't work in Safari */ text-wrap: balance; /* Prevent widows, orphans, and runts. Doesn't work in Safari */
} }

View File

@ -122,12 +122,8 @@
@media (min-width: 999px) { @media (min-width: 999px) {
border-inline-start: 1px solid var(--ifm-hover-overlay); border-inline-start: 1px solid var(--ifm-hover-overlay);
margin-inline-start: calc( margin-inline-start: calc(var(--ifm-navbar-item-padding-horizontal) / 2);
var(--ifm-navbar-item-padding-horizontal) / 2 padding-inline-start: calc(var(--ifm-navbar-item-padding-horizontal) / 2);
);
padding-inline-start: calc(
var(--ifm-navbar-item-padding-horizontal) / 2
);
} }
} }
@ -151,19 +147,14 @@
hsl(236.84deg 34.55% 10.78%) hsl(236.84deg 34.55% 10.78%)
); );
--docsearch-key-shadow: --docsearch-key-shadow:
inset 0 -2px 0 0 hsl(233.33deg 36% 24.51%), inset 0 -2px 0 0 hsl(233.33deg 36% 24.51%), inset 0 0 1px 1px hsl(232.11deg 34.86% 57.25%),
inset 0 0 1px 1px hsl(232.11deg 34.86% 57.25%),
0 2px 2px 0 rgba(3, 4, 9, 0.3); 0 2px 2px 0 rgba(3, 4, 9, 0.3);
--docsearch-key-pressed-shadow: --docsearch-key-pressed-shadow:
inset 0 -2px 0 0 #282d55, inset 0 -2px 0 0 #282d55, inset 0 0 1px 1px hsl(231.82deg 21.36% 40.39%),
inset 0 0 1px 1px hsl(231.82deg 21.36% 40.39%),
0 1px 1px 0 hsl(230deg 50% 2.35% / 30.2%); 0 1px 1px 0 hsl(230deg 50% 2.35% / 30.2%);
padding: var(--ifm-navbar-item-padding-vertical) padding: var(--ifm-navbar-item-padding-vertical) var(--ifm-navbar-item-padding-horizontal) !important;
var(--ifm-navbar-item-padding-horizontal) !important; padding-inline-end: calc(var(--ifm-navbar-item-padding-horizontal) * 1.25) !important;
padding-inline-end: calc(
var(--ifm-navbar-item-padding-horizontal) * 1.25
) !important;
.DocSearch-Button-Placeholder { .DocSearch-Button-Placeholder {
font-family: var(--ifm-heading-font-family); font-family: var(--ifm-heading-font-family);

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