Compare commits
29 Commits
docs-test-
...
monorepo-v
Author | SHA1 | Date | |
---|---|---|---|
a4f92f5e30 | |||
c15c0cbe86 | |||
c6fe0c1d85 | |||
07f0666a6f | |||
51609d696d | |||
c0d08df161 | |||
643a97f0a5 | |||
155a31fd70 | |||
c6f9d5df7b | |||
ea85331a7e | |||
4f4c5253dd | |||
83b2fc36df | |||
d99d2b8bdc | |||
9b96d04b3a | |||
ca5b99eb16 | |||
4c1676e97c | |||
81855cf2fe | |||
bd904027be | |||
0ffc97db15 | |||
2c515b1e17 | |||
f8900fbaf3 | |||
0f4a98d9c6 | |||
8853f25b45 | |||
1c40f7b95a | |||
9b5d6ec1af | |||
36d29a9ae1 | |||
0606b1aba4 | |||
03d5dad867 | |||
38a9e46af3 |
@ -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
|
||||||
|
|
||||||
|
6
.github/actions/setup/action.yml
vendored
6
.github/actions/setup/action.yml
vendored
@ -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: |
|
||||||
|
@ -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
|
||||||
|
@ -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:
|
||||||
|
6
.github/workflows/api-py-publish.yml
vendored
6
.github/workflows/api-py-publish.yml
vendored
@ -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/
|
||||||
|
7
.github/workflows/api-ts-publish.yml
vendored
7
.github/workflows/api-ts-publish.yml
vendored
@ -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/
|
||||||
|
4
.github/workflows/ci-aws-cfn.yml
vendored
4
.github/workflows/ci-aws-cfn.yml
vendored
@ -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
|
||||||
|
3
.github/workflows/ci-main-daily.yml
vendored
3
.github/workflows/ci-main-daily.yml
vendored
@ -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
|
||||||
|
62
.github/workflows/ci-main.yml
vendored
62
.github/workflows/ci-main.yml
vendored
@ -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:
|
||||||
|
32
.github/workflows/ci-outpost.yml
vendored
32
.github/workflows/ci-outpost.yml
vendored
@ -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
|
||||||
|
88
.github/workflows/ci-web.yml
vendored
88
.github/workflows/ci-web.yml
vendored
@ -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
|
|
||||||
|
66
.github/workflows/ci-website.yml
vendored
66
.github/workflows/ci-website.yml
vendored
@ -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
|
||||||
|
2
.github/workflows/codeql-analysis.yml
vendored
2
.github/workflows/codeql-analysis.yml
vendored
@ -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
|
||||||
|
@ -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:
|
||||||
|
3
.github/workflows/gha-cache-cleanup.yml
vendored
3
.github/workflows/gha-cache-cleanup.yml
vendored
@ -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
|
||||||
|
4
.github/workflows/ghcr-retention.yml
vendored
4
.github/workflows/ghcr-retention.yml
vendored
@ -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
|
||||||
|
4
.github/workflows/image-compress.yml
vendored
4
.github/workflows/image-compress.yml
vendored
@ -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: |
|
||||||
|
7
.github/workflows/publish-source-docs.yml
vendored
7
.github/workflows/publish-source-docs.yml
vendored
@ -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
|
||||||
|
3
.github/workflows/release-next-branch.yml
vendored
3
.github/workflows/release-next-branch.yml
vendored
@ -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
|
||||||
|
21
.github/workflows/release-publish.yml
vendored
21
.github/workflows/release-publish.yml
vendored
@ -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:
|
||||||
|
6
.github/workflows/release-tag.yml
vendored
6
.github/workflows/release-tag.yml
vendored
@ -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:
|
||||||
|
4
.github/workflows/repo-mirror.yml
vendored
4
.github/workflows/repo-mirror.yml
vendored
@ -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 != '' }}
|
||||||
|
3
.github/workflows/repo-stale.yml
vendored
3
.github/workflows/repo-stale.yml
vendored
@ -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:
|
||||||
|
4
.github/workflows/semgrep.yml
vendored
4
.github/workflows/semgrep.yml
vendored
@ -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
|
||||||
|
3
.github/workflows/translation-advice.yml
vendored
3
.github/workflows/translation-advice.yml
vendored
@ -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
|
||||||
|
@ -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
|
||||||
|
3
.github/workflows/translation-rename.yml
vendored
3
.github/workflows/translation-rename.yml
vendored
@ -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
23
.gitignore
vendored
@ -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
|
||||||
|
@ -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/
|
||||||
|
2
.vscode/extensions.json
vendored
2
.vscode/extensions.json
vendored
@ -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
72
.vscode/settings.json
vendored
@ -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
40
.vscode/tasks.json
vendored
@ -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"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
65
Dockerfile
65
Dockerfile
@ -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
107
Makefile
@ -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/{/{/g' diff.md
|
sed -i 's/{/{/g' diff.md
|
||||||
sed -i 's/}/}/g' diff.md
|
sed -i 's/}/}/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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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 %}
|
||||||
|
@ -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 %}
|
||||||
|
@ -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 %}
|
||||||
|
@ -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 %}
|
||||||
|
@ -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)
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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>
|
||||||
|
@ -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 %}
|
||||||
|
@ -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),
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
@ -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")
|
||||||
|
@ -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)
|
||||||
|
@ -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 %}
|
||||||
|
|
||||||
|
41
authentik/rbac/api/initial_permissions.py
Normal file
41
authentik/rbac/api/initial_permissions.py
Normal 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"]
|
39
authentik/rbac/migrations/0005_initialpermissions.py
Normal file
39
authentik/rbac/migrations/0005_initialpermissions.py
Normal 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",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
@ -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"""
|
||||||
|
@ -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)
|
||||||
|
116
authentik/rbac/tests/test_initial_permissions.py
Normal file
116
authentik/rbac/tests/test_initial_permissions.py
Normal 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))
|
@ -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"),
|
||||||
]
|
]
|
||||||
|
@ -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},
|
||||||
|
@ -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,
|
||||||
)
|
)
|
||||||
|
@ -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",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
@ -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"""
|
||||||
|
69
authentik/sources/oauth/tests/test_client.py
Normal file
69
authentik/sources/oauth/tests/test_client.py
Normal 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)
|
@ -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(
|
||||||
|
@ -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
|
||||||
|
@ -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"),
|
||||||
|
@ -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,
|
||||||
|
@ -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"),
|
||||||
|
@ -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"),
|
||||||
|
@ -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"),
|
||||||
|
@ -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)
|
||||||
|
@ -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"
|
||||||
|
@ -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"),
|
||||||
|
@ -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()
|
||||||
|
@ -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")
|
||||||
|
@ -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"),
|
||||||
|
@ -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
@ -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")]
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
10
eslint.config.mjs
Normal 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
2
go.mod
@ -27,7 +27,7 @@ require (
|
|||||||
github.com/spf13/cobra v1.9.1
|
github.com/spf13/cobra v1.9.1
|
||||||
github.com/stretchr/testify v1.10.0
|
github.com/stretchr/testify v1.10.0
|
||||||
github.com/wwt/guac v1.3.2
|
github.com/wwt/guac v1.3.2
|
||||||
goauthentik.io/api/v3 v3.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
4
go.sum
@ -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=
|
||||||
|
@ -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>
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 ""
|
||||||
|
@ -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.
@ -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 "此选项配置流程执行器页面上的页脚链接。"
|
||||||
|
@ -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
44519
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
49
package.json
49
package.json
@ -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"
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
@ -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 */
|
||||||
}
|
}
|
||||||
|
@ -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
Reference in New Issue
Block a user