Compare commits
1 Commits
monorepo-v
...
static-con
Author | SHA1 | Date | |
---|---|---|---|
86c1d60093 |
@ -10,9 +10,6 @@ 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: package.json
|
node-version-file: web/package.json
|
||||||
cache: "npm"
|
cache: "npm"
|
||||||
cache-dependency-path: package-lock.json
|
cache-dependency-path: web/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
|
||||||
npm ci
|
cd web && 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 TypeScript API Client
|
- name: generate ts 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:
|
||||||
|
7
.github/workflows/api-py-publish.yml
vendored
7
.github/workflows/api-py-publish.yml
vendored
@ -1,5 +1,4 @@
|
|||||||
name: "Python API Publish"
|
name: authentik-api-py-publish
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
@ -8,7 +7,6 @@ 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:
|
||||||
@ -32,7 +30,8 @@ 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 Python API Client
|
cache: "poetry"
|
||||||
|
- name: Generate 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: "TypeScript API Publish"
|
name: authentik-api-ts-publish
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
@ -7,7 +7,6 @@ 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:
|
||||||
@ -21,9 +20,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: package.json
|
node-version-file: web/package.json
|
||||||
registry-url: "https://registry.npmjs.org"
|
registry-url: "https://registry.npmjs.org"
|
||||||
- name: Generate TypeScript API Client
|
- name: Generate 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 CloudFormation"
|
name: authentik-ci-aws-cfn
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@ -18,7 +18,6 @@ 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
|
||||||
@ -37,7 +36,6 @@ 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,7 +9,6 @@ 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,7 +19,6 @@ env:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint:
|
lint:
|
||||||
name: "Lint"
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@ -34,10 +33,9 @@ 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 ${{ matrix.job }}
|
- name: run 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
|
||||||
@ -46,7 +44,6 @@ 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
|
||||||
@ -55,7 +52,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
|
||||||
@ -70,7 +67,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
|
||||||
@ -87,9 +84,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
|
||||||
@ -100,10 +97,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
|
||||||
@ -113,7 +110,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
uv run make ci-test
|
uv run make ci-test
|
||||||
test-unittest:
|
test-unittest:
|
||||||
name: "Unit tests - PostgreSQL ${{ matrix.psql }} - Run ${{ matrix.run_id }}/5"
|
name: test-unittest - 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
|
||||||
@ -149,7 +146,6 @@ 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:
|
||||||
@ -158,7 +154,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
|
||||||
@ -174,50 +170,49 @@ 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 Provider
|
- name: proxy
|
||||||
glob: tests/e2e/test_provider_proxy*
|
glob: tests/e2e/test_provider_proxy*
|
||||||
- name: OAuth2 Provider
|
- name: oauth
|
||||||
glob: tests/e2e/test_provider_oauth2* tests/e2e/test_source_oauth*
|
glob: tests/e2e/test_provider_oauth2* tests/e2e/test_source_oauth*
|
||||||
- name: OIDC Provider
|
- name: oauth-oidc
|
||||||
glob: tests/e2e/test_provider_oidc*
|
glob: tests/e2e/test_provider_oidc*
|
||||||
- name: SAML Provider
|
- name: saml
|
||||||
glob: tests/e2e/test_provider_saml* tests/e2e/test_source_saml*
|
glob: tests/e2e/test_provider_saml* tests/e2e/test_source_saml*
|
||||||
- name: LDAP Provider
|
- name: ldap
|
||||||
glob: tests/e2e/test_provider_ldap* tests/e2e/test_source_ldap*
|
glob: tests/e2e/test_provider_ldap* tests/e2e/test_source_ldap*
|
||||||
- name: RADIUS Provider
|
- name: radius
|
||||||
glob: tests/e2e/test_provider_radius*
|
glob: tests/e2e/test_provider_radius*
|
||||||
- name: SCIM Source
|
- name: scim
|
||||||
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('./package-lock.json', 'web/src/**') }}
|
key: ${{ runner.os }}-web-${{ hashFiles('web/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 gen-client-ts
|
make -C .. gen-client-ts
|
||||||
npm run build -w @goauthentik/web
|
npm run build
|
||||||
|
- 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
|
||||||
@ -233,7 +228,6 @@ 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
|
||||||
@ -248,7 +242,6 @@ 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
|
||||||
@ -262,7 +255,6 @@ 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
|
||||||
@ -275,7 +267,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,7 +14,6 @@ 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
|
||||||
@ -27,7 +26,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 Go API Client
|
- name: Generate API
|
||||||
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
|
||||||
@ -36,7 +35,6 @@ 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
|
||||||
@ -45,13 +43,12 @@ 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 Go API Client
|
- name: Generate API
|
||||||
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
|
||||||
@ -62,7 +59,6 @@ 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
|
||||||
@ -89,7 +85,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:
|
||||||
@ -103,7 +99,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 Go API Client
|
- name: Generate API
|
||||||
run: make gen-client-go
|
run: make gen-client-go
|
||||||
- name: Build Docker Image
|
- name: Build Docker Image
|
||||||
id: push
|
id: push
|
||||||
@ -126,7 +122,6 @@ 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
|
||||||
@ -145,22 +140,21 @@ 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"
|
||||||
- name: Generate Go API Client
|
- uses: actions/setup-node@v4
|
||||||
|
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 -w @goauthentik/web
|
npm run build-proxy
|
||||||
- name: Build outpost
|
- name: Build outpost
|
||||||
run: |
|
run: |
|
||||||
set -x
|
set -x
|
||||||
|
70
.github/workflows/ci-web.yml
vendored
70
.github/workflows/ci-web.yml
vendored
@ -1,4 +1,4 @@
|
|||||||
name: CI Web UI
|
name: authentik-ci-web
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@ -13,50 +13,54 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint:
|
lint:
|
||||||
name: Lint
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
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:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version-file: package.json
|
node-version-file: ${{ matrix.project }}/package.json
|
||||||
cache: "npm"
|
cache: "npm"
|
||||||
cache-dependency-path: package-lock.json
|
cache-dependency-path: ${{ matrix.project }}/package-lock.json
|
||||||
- name: Install Node.js dependencies
|
- working-directory: ${{ matrix.project }}/
|
||||||
run: npm ci
|
run: |
|
||||||
- name: Generate TypeScript API
|
npm ci
|
||||||
|
- name: Generate API
|
||||||
run: make gen-client-ts
|
run: make gen-client-ts
|
||||||
- name: Build
|
|
||||||
run: |
|
|
||||||
npm run build -w @goauthentik/web
|
|
||||||
- name: Type check
|
|
||||||
run: |
|
|
||||||
npm run typecheck
|
|
||||||
- name: Lint
|
- name: Lint
|
||||||
run: |
|
working-directory: ${{ matrix.project }}/
|
||||||
npm run lint -w @goauthentik/web
|
run: npm run ${{ matrix.command }}
|
||||||
npm run lint:lockfile -w @goauthentik/web
|
|
||||||
npm run lit-analyse -w @goauthentik/web
|
|
||||||
build:
|
build:
|
||||||
name: 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: package.json
|
node-version-file: web/package.json
|
||||||
cache: "npm"
|
cache: "npm"
|
||||||
cache-dependency-path: package-lock.json
|
cache-dependency-path: web/package-lock.json
|
||||||
- name: Install Node.js dependencies
|
- working-directory: web/
|
||||||
run: npm ci
|
run: npm ci
|
||||||
- name: Generate TypeScript API
|
- name: Generate API
|
||||||
run: make gen-client-ts
|
run: make gen-client-ts
|
||||||
- name: build
|
- name: build
|
||||||
run: |
|
working-directory: web/
|
||||||
npm run build -w @goauthentik/web
|
run: npm run build
|
||||||
npm run typecheck
|
|
||||||
ci-web-mark:
|
ci-web-mark:
|
||||||
name: CI Web Mark
|
|
||||||
if: always()
|
if: always()
|
||||||
needs:
|
needs:
|
||||||
- build
|
- build
|
||||||
@ -67,7 +71,6 @@ 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
|
||||||
@ -75,12 +78,13 @@ jobs:
|
|||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version-file: package.json
|
node-version-file: web/package.json
|
||||||
cache: "npm"
|
cache: "npm"
|
||||||
cache-dependency-path: package-lock.json
|
cache-dependency-path: web/package-lock.json
|
||||||
- name: Install Node.js dependencies
|
- working-directory: web/
|
||||||
run: npm ci
|
run: npm ci
|
||||||
- name: Generate TypeScript API
|
- name: Generate API
|
||||||
run: make gen-client-ts
|
run: make gen-client-ts
|
||||||
- name: Test Web UI
|
- name: test
|
||||||
run: npm run test -w @goauthentik/web || exit 0
|
working-directory: web/
|
||||||
|
run: npm run test || exit 0
|
||||||
|
94
.github/workflows/ci-website.yml
vendored
94
.github/workflows/ci-website.yml
vendored
@ -1,4 +1,4 @@
|
|||||||
name: CI Docs Website
|
name: authentik-ci-website
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@ -13,59 +13,55 @@ 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
|
||||||
- uses: actions/setup-node@v4
|
- working-directory: website/
|
||||||
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
|
|
||||||
- name: Lint Docs
|
|
||||||
run: |
|
|
||||||
npm run lint:prettier:check
|
|
||||||
npm run lint:lockfile -w @goauthentik/docs
|
|
||||||
test:
|
|
||||||
name: "Test Docs"
|
|
||||||
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
|
|
||||||
- name: Test Docs
|
|
||||||
run: |
|
|
||||||
npm run test -w @goauthentik/docs
|
|
||||||
build:
|
|
||||||
name: "Build Docs"
|
|
||||||
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
|
run: npm ci
|
||||||
- name: Build
|
- name: Lint
|
||||||
run: |
|
working-directory: website/
|
||||||
npm run build -w @goauthentik/docs
|
run: npm run ${{ matrix.command }}
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version-file: website/package.json
|
||||||
|
cache: "npm"
|
||||||
|
cache-dependency-path: website/package-lock.json
|
||||||
|
- working-directory: website/
|
||||||
|
run: npm ci
|
||||||
|
- name: test
|
||||||
|
working-directory: website/
|
||||||
|
run: npm test
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
name: ${{ matrix.job }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
job:
|
||||||
|
- build
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version-file: website/package.json
|
||||||
|
cache: "npm"
|
||||||
|
cache-dependency-path: website/package-lock.json
|
||||||
|
- working-directory: website/
|
||||||
|
run: npm ci
|
||||||
|
- name: build
|
||||||
|
working-directory: website/
|
||||||
|
run: npm run ${{ matrix.job }}
|
||||||
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 CI Update WebAuthn MDS"
|
name: authentik-gen-update-webauthn-mds
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
schedule:
|
schedule:
|
||||||
@ -11,7 +11,6 @@ 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: "Post-PR Closed Cache Cleanup"
|
name: Cleanup cache after PR is closed
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
types:
|
types:
|
||||||
@ -12,7 +12,6 @@ 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: "authentik GHCR Retention Policy"
|
name: ghcr-retention
|
||||||
|
|
||||||
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 CI Image Compression"
|
name: authentik-compress-images
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@ -20,7 +20,7 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
compress:
|
compress:
|
||||||
name: "Compress Docker images"
|
name: compress
|
||||||
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: |
|
||||||
|
45
.github/workflows/packages-npm-publish.yml
vendored
45
.github/workflows/packages-npm-publish.yml
vendored
@ -1,45 +0,0 @@
|
|||||||
name: authentik-packages-npm-publish
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [main]
|
|
||||||
paths:
|
|
||||||
- packages/docusaurus-config
|
|
||||||
- packages/eslint-config
|
|
||||||
- packages/prettier-config
|
|
||||||
- packages/tsconfig
|
|
||||||
workflow_dispatch:
|
|
||||||
jobs:
|
|
||||||
publish:
|
|
||||||
if: ${{ github.repository != 'goauthentik/authentik-internal' }}
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
package:
|
|
||||||
- docusaurus-config
|
|
||||||
- eslint-config
|
|
||||||
- prettier-config
|
|
||||||
- tsconfig
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 2
|
|
||||||
- uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version-file: packages/${{ matrix.package }}/package.json
|
|
||||||
registry-url: "https://registry.npmjs.org"
|
|
||||||
- name: Get changed files
|
|
||||||
id: changed-files
|
|
||||||
uses: tj-actions/changed-files@ed68ef82c095e0d48ec87eccea555d944a631a4c
|
|
||||||
with:
|
|
||||||
files: |
|
|
||||||
packages/${{ matrix.package }}/package.json
|
|
||||||
- name: Publish package
|
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
|
||||||
working-directory: packages/${{ matrix.package}}
|
|
||||||
run: |
|
|
||||||
npm ci
|
|
||||||
npm run build
|
|
||||||
npm publish
|
|
||||||
env:
|
|
||||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}
|
|
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,7 +12,6 @@ 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
|
||||||
@ -20,11 +19,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: Deploy to Netlify
|
- name: Publish
|
||||||
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,7 +11,6 @@ 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: "Release publish"
|
name: authentik-on-release
|
||||||
|
|
||||||
on:
|
on:
|
||||||
release:
|
release:
|
||||||
@ -7,7 +7,6 @@ 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:
|
||||||
@ -22,7 +21,6 @@ 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
|
||||||
@ -47,14 +45,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
|
||||||
@ -87,7 +85,6 @@ 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:
|
||||||
@ -109,13 +106,14 @@ 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: package.json
|
node-version-file: web/package.json
|
||||||
cache: "npm"
|
cache: "npm"
|
||||||
cache-dependency-path: package-lock.json
|
cache-dependency-path: web/package-lock.json
|
||||||
- name: Build web
|
- name: Build web
|
||||||
|
working-directory: web/
|
||||||
run: |
|
run: |
|
||||||
npm ci
|
npm ci
|
||||||
npm run build-proxy -w @goauthentik/web
|
npm run build-proxy
|
||||||
- name: Build outpost
|
- name: Build outpost
|
||||||
run: |
|
run: |
|
||||||
set -x
|
set -x
|
||||||
@ -131,7 +129,6 @@ 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
|
||||||
@ -153,7 +150,6 @@ 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
|
||||||
@ -170,7 +166,6 @@ 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
|
||||||
@ -178,7 +173,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 Release"
|
name: authentik-on-tag
|
||||||
|
|
||||||
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,15 +1,13 @@
|
|||||||
name: "authentik Repository Mirror"
|
name: "authentik-repo-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 Repository Stale Issues"
|
name: "authentik-repo-stale"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
schedule:
|
schedule:
|
||||||
@ -11,7 +11,6 @@ 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 CI Semgrep"
|
name: authentik-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 Translations Advice"
|
name: authentik-translation-advice
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
@ -16,7 +16,6 @@ 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 Extract & Compile Translations"
|
name: authentik-translate-extract-compile
|
||||||
on:
|
on:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: "0 0 * * *" # every day at midnight
|
- cron: "0 0 * * *" # every day at midnight
|
||||||
@ -16,7 +16,6 @@ env:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
compile:
|
compile:
|
||||||
name: "Compile Translations"
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- id: generate_token
|
- id: generate_token
|
||||||
@ -33,20 +32,15 @@ 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 TypeScript API
|
- name: Generate API
|
||||||
run: make gen-client-ts
|
run: make gen-client-ts
|
||||||
- name: Extract Translations
|
- name: run extract
|
||||||
run: |
|
run: |
|
||||||
uv run make i18n-extract
|
uv run make i18n-extract
|
||||||
- name: Build Docs Site
|
- name: run compile
|
||||||
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 Translations Transifex PR Rename"
|
name: authentik-translation-transifex-rename
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
@ -12,7 +12,6 @@ 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:
|
||||||
|
27
.gitignore
vendored
27
.gitignore
vendored
@ -11,10 +11,6 @@ local_settings.py
|
|||||||
db.sqlite3
|
db.sqlite3
|
||||||
media
|
media
|
||||||
|
|
||||||
# Node
|
|
||||||
|
|
||||||
node_modules
|
|
||||||
|
|
||||||
# If your build process includes running collectstatic, then you probably don't need or want to include staticfiles/
|
# If your build process includes running collectstatic, then you probably don't need or want to include staticfiles/
|
||||||
# in your Git repository. Update and uncomment the following line accordingly.
|
# in your Git repository. Update and uncomment the following line accordingly.
|
||||||
# <django-project-name>/staticfiles/
|
# <django-project-name>/staticfiles/
|
||||||
@ -217,26 +213,3 @@ 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
|
|
||||||
|
@ -1,52 +0,0 @@
|
|||||||
# Prettier Ignorefile
|
|
||||||
|
|
||||||
## Static Files
|
|
||||||
**/LICENSE
|
|
||||||
|
|
||||||
authentik/stages/**/*
|
|
||||||
authentik/sources/**/*
|
|
||||||
schemas/**/*
|
|
||||||
blueprints/**/*
|
|
||||||
|
|
||||||
## Build asset directories
|
|
||||||
coverage
|
|
||||||
dist
|
|
||||||
out
|
|
||||||
.docusaurus
|
|
||||||
.wireit
|
|
||||||
website/docs/developer-docs/api/**/*
|
|
||||||
|
|
||||||
## Environment
|
|
||||||
*.env
|
|
||||||
|
|
||||||
## Secrets
|
|
||||||
*.secrets
|
|
||||||
|
|
||||||
## Yarn
|
|
||||||
.yarn/**/*
|
|
||||||
|
|
||||||
## Node
|
|
||||||
node_modules
|
|
||||||
coverage
|
|
||||||
|
|
||||||
## Configs
|
|
||||||
*.log
|
|
||||||
*.yaml
|
|
||||||
*.yml
|
|
||||||
|
|
||||||
# Templates
|
|
||||||
# TODO: Rename affected files to *.template.* or similar.
|
|
||||||
authentik/**/*.html
|
|
||||||
*.html
|
|
||||||
*.mdx
|
|
||||||
*.md
|
|
||||||
|
|
||||||
## Import order matters
|
|
||||||
web/src/poly.ts
|
|
||||||
web/src/locale-codes.ts
|
|
||||||
web/src/locales/
|
|
||||||
|
|
||||||
# Storybook
|
|
||||||
storybook-static/
|
|
||||||
.storybook/css-import-maps*
|
|
||||||
|
|
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": "./node_modules/typescript/lib",
|
"typescript.tsdk": "./web/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,71 +30,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"go.testFlags": ["-count=1"],
|
"go.testFlags": ["-count=1"],
|
||||||
"github-actions.workflows.pinned.workflows": [".github/workflows/ci-main.yml"],
|
"github-actions.workflows.pinned.workflows": [
|
||||||
|
".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,7 +4,12 @@
|
|||||||
{
|
{
|
||||||
"label": "authentik/core: make",
|
"label": "authentik/core: make",
|
||||||
"command": "uv",
|
"command": "uv",
|
||||||
"args": ["run", "make", "lint-fix", "lint"],
|
"args": [
|
||||||
|
"run",
|
||||||
|
"make",
|
||||||
|
"lint-fix",
|
||||||
|
"lint"
|
||||||
|
],
|
||||||
"presentation": {
|
"presentation": {
|
||||||
"panel": "new"
|
"panel": "new"
|
||||||
},
|
},
|
||||||
@ -13,7 +18,11 @@
|
|||||||
{
|
{
|
||||||
"label": "authentik/core: run",
|
"label": "authentik/core: run",
|
||||||
"command": "uv",
|
"command": "uv",
|
||||||
"args": ["run", "ak", "server"],
|
"args": [
|
||||||
|
"run",
|
||||||
|
"ak",
|
||||||
|
"server"
|
||||||
|
],
|
||||||
"group": "build",
|
"group": "build",
|
||||||
"presentation": {
|
"presentation": {
|
||||||
"panel": "dedicated",
|
"panel": "dedicated",
|
||||||
@ -23,13 +32,17 @@
|
|||||||
{
|
{
|
||||||
"label": "authentik/web: make",
|
"label": "authentik/web: make",
|
||||||
"command": "make",
|
"command": "make",
|
||||||
"args": ["web"],
|
"args": [
|
||||||
|
"web"
|
||||||
|
],
|
||||||
"group": "build"
|
"group": "build"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "authentik/web: watch",
|
"label": "authentik/web: watch",
|
||||||
"command": "make",
|
"command": "make",
|
||||||
"args": ["web-watch"],
|
"args": [
|
||||||
|
"web-watch"
|
||||||
|
],
|
||||||
"group": "build",
|
"group": "build",
|
||||||
"presentation": {
|
"presentation": {
|
||||||
"panel": "dedicated",
|
"panel": "dedicated",
|
||||||
@ -39,19 +52,26 @@
|
|||||||
{
|
{
|
||||||
"label": "authentik: install",
|
"label": "authentik: install",
|
||||||
"command": "make",
|
"command": "make",
|
||||||
"args": ["install", "-j4"],
|
"args": [
|
||||||
|
"install",
|
||||||
|
"-j4"
|
||||||
|
],
|
||||||
"group": "build"
|
"group": "build"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "authentik/website: make",
|
"label": "authentik/website: make",
|
||||||
"command": "make",
|
"command": "make",
|
||||||
"args": ["website"],
|
"args": [
|
||||||
|
"website"
|
||||||
|
],
|
||||||
"group": "build"
|
"group": "build"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "authentik/website: watch",
|
"label": "authentik/website: watch",
|
||||||
"command": "make",
|
"command": "make",
|
||||||
"args": ["website-watch"],
|
"args": [
|
||||||
|
"website-watch"
|
||||||
|
],
|
||||||
"group": "build",
|
"group": "build",
|
||||||
"presentation": {
|
"presentation": {
|
||||||
"panel": "dedicated",
|
"panel": "dedicated",
|
||||||
@ -61,7 +81,11 @@
|
|||||||
{
|
{
|
||||||
"label": "authentik/api: generate",
|
"label": "authentik/api: generate",
|
||||||
"command": "uv",
|
"command": "uv",
|
||||||
"args": ["run", "make", "gen"],
|
"args": [
|
||||||
|
"run",
|
||||||
|
"make",
|
||||||
|
"gen"
|
||||||
|
],
|
||||||
"group": "build"
|
"group": "build"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -23,8 +23,6 @@ docker-compose.yml @goauthentik/infrastructure
|
|||||||
Makefile @goauthentik/infrastructure
|
Makefile @goauthentik/infrastructure
|
||||||
.editorconfig @goauthentik/infrastructure
|
.editorconfig @goauthentik/infrastructure
|
||||||
CODEOWNERS @goauthentik/infrastructure
|
CODEOWNERS @goauthentik/infrastructure
|
||||||
# Web packages
|
|
||||||
packages/ @goauthentik/frontend
|
|
||||||
# Web
|
# Web
|
||||||
web/ @goauthentik/frontend
|
web/ @goauthentik/frontend
|
||||||
tests/wdio/ @goauthentik/frontend
|
tests/wdio/ @goauthentik/frontend
|
||||||
|
67
Dockerfile
67
Dockerfile
@ -1,31 +1,48 @@
|
|||||||
# syntax=docker/dockerfile:1
|
# syntax=docker/dockerfile:1
|
||||||
|
|
||||||
# Stage 1 Web UI and Documentation build
|
# Stage 1: Build website
|
||||||
|
FROM --platform=${BUILDPLATFORM} docker.io/library/node:22 AS website-builder
|
||||||
FROM --platform=${BUILDPLATFORM} docker.io/library/node:22 AS web-builder
|
|
||||||
|
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
WORKDIR /work
|
WORKDIR /work/website
|
||||||
|
|
||||||
COPY ./package.json ./package.json
|
RUN --mount=type=bind,target=/work/website/package.json,src=./website/package.json \
|
||||||
COPY ./package-lock.json ./package-lock.json
|
--mount=type=bind,target=/work/website/package-lock.json,src=./website/package-lock.json \
|
||||||
COPY ./packages ./packages
|
--mount=type=cache,id=npm-website,sharing=shared,target=/root/.npm \
|
||||||
COPY ./web ./web
|
npm ci --include=dev
|
||||||
COPY ./website ./website
|
|
||||||
|
|
||||||
COPY ./gen-ts-api ./gen-ts-api
|
COPY ./website /work/website/
|
||||||
COPY ./blueprints ./blueprints
|
COPY ./blueprints /work/blueprints/
|
||||||
COPY ./schema.yml ./schema.yml
|
COPY ./schema.yml /work/
|
||||||
COPY ./SECURITY.md ./SECURITY.md
|
COPY ./SECURITY.md /work/
|
||||||
|
|
||||||
RUN --mount=type=cache,target=/root/.npm npm ci --include=dev
|
RUN npm run build-bundled
|
||||||
|
|
||||||
RUN npm run build-bundled -w @goauthentik/docs
|
# Stage 2: Build webui
|
||||||
RUN npm run build -w @goauthentik/web
|
FROM --platform=${BUILDPLATFORM} docker.io/library/node:22 AS web-builder
|
||||||
|
|
||||||
# Stage 2: Build go proxy
|
ARG GIT_BUILD_HASH
|
||||||
|
ENV GIT_BUILD_HASH=$GIT_BUILD_HASH
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
|
WORKDIR /work/web
|
||||||
|
|
||||||
|
RUN --mount=type=bind,target=/work/web/package.json,src=./web/package.json \
|
||||||
|
--mount=type=bind,target=/work/web/package-lock.json,src=./web/package-lock.json \
|
||||||
|
--mount=type=bind,target=/work/web/packages/sfe/package.json,src=./web/packages/sfe/package.json \
|
||||||
|
--mount=type=bind,target=/work/web/scripts,src=./web/scripts \
|
||||||
|
--mount=type=cache,id=npm-web,sharing=shared,target=/root/.npm \
|
||||||
|
npm ci --include=dev
|
||||||
|
|
||||||
|
COPY ./package.json /work
|
||||||
|
COPY ./web /work/web/
|
||||||
|
COPY ./website /work/website/
|
||||||
|
COPY ./gen-ts-api /work/web/node_modules/@goauthentik/api
|
||||||
|
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# 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
|
||||||
@ -62,8 +79,7 @@ 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 3: MaxMind GeoIP
|
# Stage 4: 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"
|
||||||
@ -77,11 +93,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 4: Download uv
|
# Stage 5: 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.9-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" \
|
||||||
PATH="/lifecycle:/ak-root/.venv/bin:$PATH" \
|
PATH="/lifecycle:/ak-root/.venv/bin:$PATH" \
|
||||||
@ -94,7 +109,7 @@ WORKDIR /ak-root/
|
|||||||
|
|
||||||
COPY --from=uv /uv /uvx /bin/
|
COPY --from=uv /uv /uvx /bin/
|
||||||
|
|
||||||
# Stage 6: Python dependencies
|
# Stage 7: Python dependencies
|
||||||
FROM python-base AS python-deps
|
FROM python-base AS python-deps
|
||||||
|
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
@ -129,7 +144,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 7: Run
|
# Stage 8: Run
|
||||||
FROM python-base AS final-image
|
FROM python-base AS final-image
|
||||||
|
|
||||||
ARG VERSION
|
ARG VERSION
|
||||||
@ -174,7 +189,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=web-builder /work/website/build/ /website/help/
|
COPY --from=website-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,13 +36,6 @@ 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)
|
||||||
@ -54,6 +47,9 @@ 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
|
||||||
|
|
||||||
@ -76,9 +72,7 @@ core-i18n-extract:
|
|||||||
--ignore website \
|
--ignore website \
|
||||||
-l en
|
-l en
|
||||||
|
|
||||||
install: ## Install all requires dependencies for `web`, `website` and `core`
|
install: web-install website-install 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}
|
||||||
@ -100,7 +94,6 @@ 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 \
|
||||||
@ -108,24 +101,19 @@ 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
|
||||||
@ -145,57 +133,46 @@ 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 \
|
||||||
--input-spec /local/schema.yml \
|
-i /local/schema.yml \
|
||||||
--generator-name typescript-fetch \
|
-g typescript-fetch \
|
||||||
--output /local/${GEN_API_TS} \
|
-o /local/${GEN_API_TS} \
|
||||||
--config /local/scripts/api-ts-config.yaml \
|
-c /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
|
||||||
npm install
|
cd ./${GEN_API_TS} && npm i
|
||||||
|
\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 \
|
||||||
--input-spec /local/schema.yml \
|
-i /local/schema.yml \
|
||||||
--generator-name python \
|
-g python \
|
||||||
--output /local/${GEN_API_PY} \
|
-o /local/${GEN_API_PY} \
|
||||||
--config /local/scripts/api-py-config.yaml \
|
-c /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/config.yaml \
|
wget https://raw.githubusercontent.com/goauthentik/client-go/main/templates/README.mustache -O ./${GEN_API_GO}/templates/README.mustache
|
||||||
-O ./${GEN_API_GO}/config.yaml
|
wget https://raw.githubusercontent.com/goauthentik/client-go/main/templates/go.mod.mustache -O ./${GEN_API_GO}/templates/go.mod.mustache
|
||||||
|
|
||||||
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 \
|
||||||
--input-spec /local/schema.yml \
|
-i /local/schema.yml \
|
||||||
--generator-name go \
|
-g go \
|
||||||
--output /local/ \
|
-o /local/ \
|
||||||
--config /local/config.yaml
|
-c /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
|
||||||
@ -207,38 +184,56 @@ gen: gen-build gen-client-ts
|
|||||||
## Web
|
## Web
|
||||||
#########################
|
#########################
|
||||||
|
|
||||||
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
|
web-build: web-install ## Build the Authentik UI
|
||||||
|
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
|
||||||
npm run test -w @goauthentik/web
|
cd web && npm run test
|
||||||
|
|
||||||
web-watch: ## Build and watch the Authentik UI for changes, updating automatically
|
web-watch: ## Build and watch the Authentik UI for changes, updating automatically
|
||||||
npm run watch -w @goauthentik/web
|
rm -rf web/dist/
|
||||||
|
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
|
||||||
npm run storybook -w @goauthentik/web
|
cd web && npm run storybook
|
||||||
|
|
||||||
web-lint-fix:
|
web-lint-fix:
|
||||||
npm run prettier -w @goauthentik/web
|
cd web && npm run prettier
|
||||||
|
|
||||||
web-lint:
|
web-lint:
|
||||||
npm run lint -w @goauthentik/web
|
cd web && npm run lint
|
||||||
npm run lit-analyse -w @goauthentik/web
|
cd web && npm run lit-analyse
|
||||||
|
|
||||||
|
web-check-compile:
|
||||||
|
cd web && npm run tsc
|
||||||
|
|
||||||
web-i18n-extract:
|
web-i18n-extract:
|
||||||
npm run extract-locales -w @goauthentik/web
|
cd web && npm run extract-locales
|
||||||
|
|
||||||
#########################
|
#########################
|
||||||
## Website
|
## Website
|
||||||
#########################
|
#########################
|
||||||
|
|
||||||
website: node-lint-fix website-build ## Automatically fix formatting issues in the Authentik website/docs source code, lint the code, and compile it
|
website: website-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:
|
||||||
npm run build -w @goauthentik/docs
|
cd website && npm run build
|
||||||
|
|
||||||
website-watch: ## Build and watch the documentation website, updating automatically
|
website-watch: ## Build and watch the documentation website, updating automatically
|
||||||
npm run watch -w @goauthentik/docs
|
cd website && npm run watch
|
||||||
|
|
||||||
#########################
|
#########################
|
||||||
## 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
|
from rest_framework.serializers import ListSerializer, ModelSerializer
|
||||||
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, ModelSerializer, PassiveSerializer
|
from authentik.core.api.utils import JSONDictField, PassiveSerializer
|
||||||
from authentik.rbac.decorators import permission_required
|
from authentik.rbac.decorators import permission_required
|
||||||
|
|
||||||
|
|
||||||
|
@ -36,7 +36,6 @@ from authentik.core.models import (
|
|||||||
GroupSourceConnection,
|
GroupSourceConnection,
|
||||||
PropertyMapping,
|
PropertyMapping,
|
||||||
Provider,
|
Provider,
|
||||||
Session,
|
|
||||||
Source,
|
Source,
|
||||||
User,
|
User,
|
||||||
UserSourceConnection,
|
UserSourceConnection,
|
||||||
@ -109,7 +108,6 @@ def excluded_models() -> list[type[Model]]:
|
|||||||
Policy,
|
Policy,
|
||||||
PolicyBindingModel,
|
PolicyBindingModel,
|
||||||
# Classes that have other dependencies
|
# Classes that have other dependencies
|
||||||
Session,
|
|
||||||
AuthenticatedSession,
|
AuthenticatedSession,
|
||||||
# Classes which are only internally managed
|
# Classes which are only internally managed
|
||||||
# FIXME: these shouldn't need to be explicitly listed, but rather based off of a mixin
|
# FIXME: these shouldn't need to be explicitly listed, but rather based off of a mixin
|
||||||
|
@ -5,7 +5,6 @@ from typing import TypedDict
|
|||||||
from rest_framework import mixins
|
from rest_framework import mixins
|
||||||
from rest_framework.fields import SerializerMethodField
|
from rest_framework.fields import SerializerMethodField
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
from rest_framework.serializers import CharField, DateTimeField, IPAddressField
|
|
||||||
from rest_framework.viewsets import GenericViewSet
|
from rest_framework.viewsets import GenericViewSet
|
||||||
from ua_parser import user_agent_parser
|
from ua_parser import user_agent_parser
|
||||||
|
|
||||||
@ -55,11 +54,6 @@ class UserAgentDict(TypedDict):
|
|||||||
class AuthenticatedSessionSerializer(ModelSerializer):
|
class AuthenticatedSessionSerializer(ModelSerializer):
|
||||||
"""AuthenticatedSession Serializer"""
|
"""AuthenticatedSession Serializer"""
|
||||||
|
|
||||||
expires = DateTimeField(source="session.expires", read_only=True)
|
|
||||||
last_ip = IPAddressField(source="session.last_ip", read_only=True)
|
|
||||||
last_user_agent = CharField(source="session.last_user_agent", read_only=True)
|
|
||||||
last_used = DateTimeField(source="session.last_used", read_only=True)
|
|
||||||
|
|
||||||
current = SerializerMethodField()
|
current = SerializerMethodField()
|
||||||
user_agent = SerializerMethodField()
|
user_agent = SerializerMethodField()
|
||||||
geo_ip = SerializerMethodField()
|
geo_ip = SerializerMethodField()
|
||||||
@ -68,19 +62,19 @@ class AuthenticatedSessionSerializer(ModelSerializer):
|
|||||||
def get_current(self, instance: AuthenticatedSession) -> bool:
|
def get_current(self, instance: AuthenticatedSession) -> bool:
|
||||||
"""Check if session is currently active session"""
|
"""Check if session is currently active session"""
|
||||||
request: Request = self.context["request"]
|
request: Request = self.context["request"]
|
||||||
return request._request.session.session_key == instance.session.session_key
|
return request._request.session.session_key == instance.session_key
|
||||||
|
|
||||||
def get_user_agent(self, instance: AuthenticatedSession) -> UserAgentDict:
|
def get_user_agent(self, instance: AuthenticatedSession) -> UserAgentDict:
|
||||||
"""Get parsed user agent"""
|
"""Get parsed user agent"""
|
||||||
return user_agent_parser.Parse(instance.session.last_user_agent)
|
return user_agent_parser.Parse(instance.last_user_agent)
|
||||||
|
|
||||||
def get_geo_ip(self, instance: AuthenticatedSession) -> GeoIPDict | None: # pragma: no cover
|
def get_geo_ip(self, instance: AuthenticatedSession) -> GeoIPDict | None: # pragma: no cover
|
||||||
"""Get GeoIP Data"""
|
"""Get GeoIP Data"""
|
||||||
return GEOIP_CONTEXT_PROCESSOR.city_dict(instance.session.last_ip)
|
return GEOIP_CONTEXT_PROCESSOR.city_dict(instance.last_ip)
|
||||||
|
|
||||||
def get_asn(self, instance: AuthenticatedSession) -> ASNDict | None: # pragma: no cover
|
def get_asn(self, instance: AuthenticatedSession) -> ASNDict | None: # pragma: no cover
|
||||||
"""Get ASN Data"""
|
"""Get ASN Data"""
|
||||||
return ASN_CONTEXT_PROCESSOR.asn_dict(instance.session.last_ip)
|
return ASN_CONTEXT_PROCESSOR.asn_dict(instance.last_ip)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = AuthenticatedSession
|
model = AuthenticatedSession
|
||||||
@ -96,7 +90,6 @@ class AuthenticatedSessionSerializer(ModelSerializer):
|
|||||||
"last_used",
|
"last_used",
|
||||||
"expires",
|
"expires",
|
||||||
]
|
]
|
||||||
extra_args = {"uuid": {"read_only": True}}
|
|
||||||
|
|
||||||
|
|
||||||
class AuthenticatedSessionViewSet(
|
class AuthenticatedSessionViewSet(
|
||||||
@ -108,10 +101,9 @@ class AuthenticatedSessionViewSet(
|
|||||||
):
|
):
|
||||||
"""AuthenticatedSession Viewset"""
|
"""AuthenticatedSession Viewset"""
|
||||||
|
|
||||||
lookup_field = "uuid"
|
queryset = AuthenticatedSession.objects.all()
|
||||||
queryset = AuthenticatedSession.objects.select_related("session").all()
|
|
||||||
serializer_class = AuthenticatedSessionSerializer
|
serializer_class = AuthenticatedSessionSerializer
|
||||||
search_fields = ["user__username", "session__last_ip", "session__last_user_agent"]
|
search_fields = ["user__username", "last_ip", "last_user_agent"]
|
||||||
filterset_fields = ["user__username", "session__last_ip", "session__last_user_agent"]
|
filterset_fields = ["user__username", "last_ip", "last_user_agent"]
|
||||||
ordering = ["user__username"]
|
ordering = ["user__username"]
|
||||||
owner_field = "user"
|
owner_field = "user"
|
||||||
|
@ -1,11 +1,14 @@
|
|||||||
"""User API Views"""
|
"""User API Views"""
|
||||||
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
from importlib import import_module
|
||||||
from json import loads
|
from json import loads
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
from django.contrib.auth import update_session_auth_hash
|
from django.contrib.auth import update_session_auth_hash
|
||||||
from django.contrib.auth.models import Permission
|
from django.contrib.auth.models import Permission
|
||||||
|
from django.contrib.sessions.backends.base import SessionBase
|
||||||
from django.db.models.functions import ExtractHour
|
from django.db.models.functions import ExtractHour
|
||||||
from django.db.transaction import atomic
|
from django.db.transaction import atomic
|
||||||
from django.db.utils import IntegrityError
|
from django.db.utils import IntegrityError
|
||||||
@ -69,8 +72,8 @@ from authentik.core.middleware import (
|
|||||||
from authentik.core.models import (
|
from authentik.core.models import (
|
||||||
USER_ATTRIBUTE_TOKEN_EXPIRING,
|
USER_ATTRIBUTE_TOKEN_EXPIRING,
|
||||||
USER_PATH_SERVICE_ACCOUNT,
|
USER_PATH_SERVICE_ACCOUNT,
|
||||||
|
AuthenticatedSession,
|
||||||
Group,
|
Group,
|
||||||
Session,
|
|
||||||
Token,
|
Token,
|
||||||
TokenIntents,
|
TokenIntents,
|
||||||
User,
|
User,
|
||||||
@ -89,6 +92,7 @@ from authentik.stages.email.tasks import send_mails
|
|||||||
from authentik.stages.email.utils import TemplateEmailMessage
|
from authentik.stages.email.utils import TemplateEmailMessage
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
SessionStore: SessionBase = import_module(settings.SESSION_ENGINE).SessionStore
|
||||||
|
|
||||||
|
|
||||||
class UserGroupSerializer(ModelSerializer):
|
class UserGroupSerializer(ModelSerializer):
|
||||||
@ -772,6 +776,10 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
|||||||
response = super().partial_update(request, *args, **kwargs)
|
response = super().partial_update(request, *args, **kwargs)
|
||||||
instance: User = self.get_object()
|
instance: User = self.get_object()
|
||||||
if not instance.is_active:
|
if not instance.is_active:
|
||||||
Session.objects.filter(authenticatedsession__user=instance).delete()
|
sessions = AuthenticatedSession.objects.filter(user=instance)
|
||||||
|
session_ids = sessions.values_list("session_key", flat=True)
|
||||||
|
for session in session_ids:
|
||||||
|
SessionStore(session).delete()
|
||||||
|
sessions.delete()
|
||||||
LOGGER.debug("Deleted user's sessions", user=instance.username)
|
LOGGER.debug("Deleted user's sessions", user=instance.username)
|
||||||
return response
|
return response
|
||||||
|
@ -20,8 +20,6 @@ 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"""
|
||||||
@ -31,14 +29,6 @@ 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)
|
||||||
|
@ -24,15 +24,6 @@ class InbuiltBackend(ModelBackend):
|
|||||||
self.set_method("password", request)
|
self.set_method("password", request)
|
||||||
return user
|
return user
|
||||||
|
|
||||||
async def aauthenticate(
|
|
||||||
self, request: HttpRequest, username: str | None, password: str | None, **kwargs: Any
|
|
||||||
) -> User | None:
|
|
||||||
user = await super().aauthenticate(request, username=username, password=password, **kwargs)
|
|
||||||
if not user:
|
|
||||||
return None
|
|
||||||
self.set_method("password", request)
|
|
||||||
return user
|
|
||||||
|
|
||||||
def set_method(self, method: str, request: HttpRequest | None, **kwargs):
|
def set_method(self, method: str, request: HttpRequest | None, **kwargs):
|
||||||
"""Set method data on current flow, if possbiel"""
|
"""Set method data on current flow, if possbiel"""
|
||||||
if not request:
|
if not request:
|
||||||
|
@ -1,15 +0,0 @@
|
|||||||
"""Change user type"""
|
|
||||||
|
|
||||||
from importlib import import_module
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
|
|
||||||
from authentik.tenants.management import TenantCommand
|
|
||||||
|
|
||||||
|
|
||||||
class Command(TenantCommand):
|
|
||||||
"""Delete all sessions"""
|
|
||||||
|
|
||||||
def handle_per_tenant(self, **options):
|
|
||||||
engine = import_module(settings.SESSION_ENGINE)
|
|
||||||
engine.SessionStore.clear_expired()
|
|
@ -2,14 +2,9 @@
|
|||||||
|
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from contextvars import ContextVar
|
from contextvars import ContextVar
|
||||||
from functools import partial
|
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from django.contrib.auth.models import AnonymousUser
|
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
|
||||||
from django.http import HttpRequest, HttpResponse
|
from django.http import HttpRequest, HttpResponse
|
||||||
from django.utils.deprecation import MiddlewareMixin
|
|
||||||
from django.utils.functional import SimpleLazyObject
|
|
||||||
from django.utils.translation import override
|
from django.utils.translation import override
|
||||||
from sentry_sdk.api import set_tag
|
from sentry_sdk.api import set_tag
|
||||||
from structlog.contextvars import STRUCTLOG_KEY_PREFIX
|
from structlog.contextvars import STRUCTLOG_KEY_PREFIX
|
||||||
@ -25,40 +20,6 @@ CTX_HOST = ContextVar[str | None](STRUCTLOG_KEY_PREFIX + "host", default=None)
|
|||||||
CTX_AUTH_VIA = ContextVar[str | None](STRUCTLOG_KEY_PREFIX + KEY_AUTH_VIA, default=None)
|
CTX_AUTH_VIA = ContextVar[str | None](STRUCTLOG_KEY_PREFIX + KEY_AUTH_VIA, default=None)
|
||||||
|
|
||||||
|
|
||||||
def get_user(request):
|
|
||||||
if not hasattr(request, "_cached_user"):
|
|
||||||
user = None
|
|
||||||
if (authenticated_session := request.session.get("authenticatedsession", None)) is not None:
|
|
||||||
user = authenticated_session.user
|
|
||||||
request._cached_user = user or AnonymousUser()
|
|
||||||
return request._cached_user
|
|
||||||
|
|
||||||
|
|
||||||
async def aget_user(request):
|
|
||||||
if not hasattr(request, "_cached_user"):
|
|
||||||
user = None
|
|
||||||
if (
|
|
||||||
authenticated_session := await request.session.aget("authenticatedsession", None)
|
|
||||||
) is not None:
|
|
||||||
user = authenticated_session.user
|
|
||||||
request._cached_user = user or AnonymousUser()
|
|
||||||
return request._cached_user
|
|
||||||
|
|
||||||
|
|
||||||
class AuthenticationMiddleware(MiddlewareMixin):
|
|
||||||
def process_request(self, request):
|
|
||||||
if not hasattr(request, "session"):
|
|
||||||
raise ImproperlyConfigured(
|
|
||||||
"The Django authentication middleware requires session "
|
|
||||||
"middleware to be installed. Edit your MIDDLEWARE setting to "
|
|
||||||
"insert "
|
|
||||||
"'authentik.root.middleware.SessionMiddleware' before "
|
|
||||||
"'authentik.core.middleware.AuthenticationMiddleware'."
|
|
||||||
)
|
|
||||||
request.user = SimpleLazyObject(lambda: get_user(request))
|
|
||||||
request.auser = partial(aget_user, request)
|
|
||||||
|
|
||||||
|
|
||||||
class ImpersonateMiddleware:
|
class ImpersonateMiddleware:
|
||||||
"""Middleware to impersonate users"""
|
"""Middleware to impersonate users"""
|
||||||
|
|
||||||
|
@ -1,238 +0,0 @@
|
|||||||
# Generated by Django 5.0.11 on 2025-01-27 12:58
|
|
||||||
|
|
||||||
import uuid
|
|
||||||
import pickle # nosec
|
|
||||||
from django.core import signing
|
|
||||||
from django.contrib.auth import BACKEND_SESSION_KEY, HASH_SESSION_KEY, SESSION_KEY
|
|
||||||
from django.db import migrations, models
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.conf import settings
|
|
||||||
from django.contrib.sessions.backends.cache import KEY_PREFIX
|
|
||||||
from django.utils.timezone import now, timedelta
|
|
||||||
from authentik.lib.migrations import progress_bar
|
|
||||||
from authentik.root.middleware import ClientIPMiddleware
|
|
||||||
|
|
||||||
|
|
||||||
SESSION_CACHE_ALIAS = "default"
|
|
||||||
|
|
||||||
|
|
||||||
class PickleSerializer:
|
|
||||||
"""
|
|
||||||
Simple wrapper around pickle to be used in signing.dumps()/loads() and
|
|
||||||
cache backends.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, protocol=None):
|
|
||||||
self.protocol = pickle.HIGHEST_PROTOCOL if protocol is None else protocol
|
|
||||||
|
|
||||||
def dumps(self, obj):
|
|
||||||
"""Pickle data to be stored in redis"""
|
|
||||||
return pickle.dumps(obj, self.protocol)
|
|
||||||
|
|
||||||
def loads(self, data):
|
|
||||||
"""Unpickle data to be loaded from redis"""
|
|
||||||
return pickle.loads(data) # nosec
|
|
||||||
|
|
||||||
|
|
||||||
def _migrate_session(
|
|
||||||
apps,
|
|
||||||
db_alias,
|
|
||||||
session_key,
|
|
||||||
session_data,
|
|
||||||
expires,
|
|
||||||
):
|
|
||||||
Session = apps.get_model("authentik_core", "Session")
|
|
||||||
OldAuthenticatedSession = apps.get_model("authentik_core", "OldAuthenticatedSession")
|
|
||||||
AuthenticatedSession = apps.get_model("authentik_core", "AuthenticatedSession")
|
|
||||||
|
|
||||||
old_auth_session = (
|
|
||||||
OldAuthenticatedSession.objects.using(db_alias).filter(session_key=session_key).first()
|
|
||||||
)
|
|
||||||
|
|
||||||
args = {
|
|
||||||
"session_key": session_key,
|
|
||||||
"expires": expires,
|
|
||||||
"last_ip": ClientIPMiddleware.default_ip,
|
|
||||||
"last_user_agent": "",
|
|
||||||
"session_data": {},
|
|
||||||
}
|
|
||||||
for k, v in session_data.items():
|
|
||||||
if k == "authentik/stages/user_login/last_ip":
|
|
||||||
args["last_ip"] = v
|
|
||||||
elif k in ["last_user_agent", "last_used"]:
|
|
||||||
args[k] = v
|
|
||||||
elif args in [SESSION_KEY, BACKEND_SESSION_KEY, HASH_SESSION_KEY]:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
args["session_data"][k] = v
|
|
||||||
if old_auth_session:
|
|
||||||
args["last_user_agent"] = old_auth_session.last_user_agent
|
|
||||||
args["last_used"] = old_auth_session.last_used
|
|
||||||
|
|
||||||
args["session_data"] = pickle.dumps(args["session_data"])
|
|
||||||
session = Session.objects.using(db_alias).create(**args)
|
|
||||||
|
|
||||||
if old_auth_session:
|
|
||||||
AuthenticatedSession.objects.using(db_alias).create(
|
|
||||||
session=session,
|
|
||||||
user=old_auth_session.user,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def migrate_redis_sessions(apps, schema_editor):
|
|
||||||
from django.core.cache import caches
|
|
||||||
|
|
||||||
db_alias = schema_editor.connection.alias
|
|
||||||
cache = caches[SESSION_CACHE_ALIAS]
|
|
||||||
|
|
||||||
# Not a redis cache, skipping
|
|
||||||
if not hasattr(cache, "keys"):
|
|
||||||
return
|
|
||||||
|
|
||||||
print("\nMigrating Redis sessions to database, this might take a couple of minutes...")
|
|
||||||
for key, session_data in progress_bar(cache.get_many(cache.keys(f"{KEY_PREFIX}*")).items()):
|
|
||||||
_migrate_session(
|
|
||||||
apps=apps,
|
|
||||||
db_alias=db_alias,
|
|
||||||
session_key=key.removeprefix(KEY_PREFIX),
|
|
||||||
session_data=session_data,
|
|
||||||
expires=now() + timedelta(seconds=cache.ttl(key)),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def migrate_database_sessions(apps, schema_editor):
|
|
||||||
DjangoSession = apps.get_model("sessions", "Session")
|
|
||||||
db_alias = schema_editor.connection.alias
|
|
||||||
|
|
||||||
print("\nMigration database sessions, this might take a couple of minutes...")
|
|
||||||
for django_session in progress_bar(DjangoSession.objects.using(db_alias).all()):
|
|
||||||
session_data = signing.loads(
|
|
||||||
django_session.session_data,
|
|
||||||
salt="django.contrib.sessions.SessionStore",
|
|
||||||
serializer=PickleSerializer,
|
|
||||||
)
|
|
||||||
_migrate_session(
|
|
||||||
apps=apps,
|
|
||||||
db_alias=db_alias,
|
|
||||||
session_key=django_session.session_key,
|
|
||||||
session_data=session_data,
|
|
||||||
expires=django_session.expire_date,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("sessions", "0001_initial"),
|
|
||||||
("authentik_core", "0045_rename_new_identifier_usersourceconnection_identifier_and_more"),
|
|
||||||
("authentik_providers_oauth2", "0027_accesstoken_authentik_p_expires_9f24a5_idx_and_more"),
|
|
||||||
("authentik_providers_rac", "0006_connectiontoken_authentik_p_expires_91f148_idx_and_more"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
# Rename AuthenticatedSession to OldAuthenticatedSession
|
|
||||||
migrations.RenameModel(
|
|
||||||
old_name="AuthenticatedSession",
|
|
||||||
new_name="OldAuthenticatedSession",
|
|
||||||
),
|
|
||||||
migrations.RenameIndex(
|
|
||||||
model_name="oldauthenticatedsession",
|
|
||||||
new_name="authentik_c_expires_cf4f72_idx",
|
|
||||||
old_name="authentik_c_expires_08251d_idx",
|
|
||||||
),
|
|
||||||
migrations.RenameIndex(
|
|
||||||
model_name="oldauthenticatedsession",
|
|
||||||
new_name="authentik_c_expirin_c1f17f_idx",
|
|
||||||
old_name="authentik_c_expirin_9cd839_idx",
|
|
||||||
),
|
|
||||||
migrations.RenameIndex(
|
|
||||||
model_name="oldauthenticatedsession",
|
|
||||||
new_name="authentik_c_expirin_e04f5d_idx",
|
|
||||||
old_name="authentik_c_expirin_195a84_idx",
|
|
||||||
),
|
|
||||||
migrations.RenameIndex(
|
|
||||||
model_name="oldauthenticatedsession",
|
|
||||||
new_name="authentik_c_session_a44819_idx",
|
|
||||||
old_name="authentik_c_session_d0f005_idx",
|
|
||||||
),
|
|
||||||
migrations.RunSQL(
|
|
||||||
sql="ALTER INDEX authentik_core_authenticatedsession_user_id_5055b6cf RENAME TO authentik_core_oldauthenticatedsession_user_id_5055b6cf",
|
|
||||||
reverse_sql="ALTER INDEX authentik_core_oldauthenticatedsession_user_id_5055b6cf RENAME TO authentik_core_authenticatedsession_user_id_5055b6cf",
|
|
||||||
),
|
|
||||||
# Create new Session and AuthenticatedSession models
|
|
||||||
migrations.CreateModel(
|
|
||||||
name="Session",
|
|
||||||
fields=[
|
|
||||||
(
|
|
||||||
"session_key",
|
|
||||||
models.CharField(
|
|
||||||
max_length=40, primary_key=True, serialize=False, verbose_name="session key"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
("expires", models.DateTimeField(default=None, null=True)),
|
|
||||||
("expiring", models.BooleanField(default=True)),
|
|
||||||
("session_data", models.BinaryField(verbose_name="session data")),
|
|
||||||
("last_ip", models.GenericIPAddressField()),
|
|
||||||
("last_user_agent", models.TextField(blank=True)),
|
|
||||||
("last_used", models.DateTimeField(auto_now=True)),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
"default_permissions": [],
|
|
||||||
"verbose_name": "Session",
|
|
||||||
"verbose_name_plural": "Sessions",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.AddIndex(
|
|
||||||
model_name="session",
|
|
||||||
index=models.Index(fields=["expires"], name="authentik_c_expires_d2f607_idx"),
|
|
||||||
),
|
|
||||||
migrations.AddIndex(
|
|
||||||
model_name="session",
|
|
||||||
index=models.Index(fields=["expiring"], name="authentik_c_expirin_7c2cfb_idx"),
|
|
||||||
),
|
|
||||||
migrations.AddIndex(
|
|
||||||
model_name="session",
|
|
||||||
index=models.Index(
|
|
||||||
fields=["expiring", "expires"], name="authentik_c_expirin_1ab2e4_idx"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AddIndex(
|
|
||||||
model_name="session",
|
|
||||||
index=models.Index(
|
|
||||||
fields=["expires", "session_key"], name="authentik_c_expires_c49143_idx"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name="AuthenticatedSession",
|
|
||||||
fields=[
|
|
||||||
(
|
|
||||||
"session",
|
|
||||||
models.OneToOneField(
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
primary_key=True,
|
|
||||||
serialize=False,
|
|
||||||
to="authentik_core.session",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
("uuid", models.UUIDField(default=uuid.uuid4, unique=True)),
|
|
||||||
(
|
|
||||||
"user",
|
|
||||||
models.ForeignKey(
|
|
||||||
on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
"verbose_name": "Authenticated Session",
|
|
||||||
"verbose_name_plural": "Authenticated Sessions",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.RunPython(
|
|
||||||
code=migrate_redis_sessions,
|
|
||||||
reverse_code=migrations.RunPython.noop,
|
|
||||||
),
|
|
||||||
migrations.RunPython(
|
|
||||||
code=migrate_database_sessions,
|
|
||||||
reverse_code=migrations.RunPython.noop,
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,18 +0,0 @@
|
|||||||
# Generated by Django 5.0.11 on 2025-01-27 13:02
|
|
||||||
|
|
||||||
from django.db import migrations
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("authentik_core", "0046_session_and_more"),
|
|
||||||
("authentik_providers_rac", "0007_migrate_session"),
|
|
||||||
("authentik_providers_oauth2", "0028_migrate_session"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.DeleteModel(
|
|
||||||
name="OldAuthenticatedSession",
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,7 +1,6 @@
|
|||||||
"""authentik core models"""
|
"""authentik core models"""
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from enum import StrEnum
|
|
||||||
from hashlib import sha256
|
from hashlib import sha256
|
||||||
from typing import Any, Optional, Self
|
from typing import Any, Optional, Self
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
@ -10,7 +9,6 @@ from deepmerge import always_merger
|
|||||||
from django.contrib.auth.hashers import check_password
|
from django.contrib.auth.hashers import check_password
|
||||||
from django.contrib.auth.models import AbstractUser
|
from django.contrib.auth.models import AbstractUser
|
||||||
from django.contrib.auth.models import UserManager as DjangoUserManager
|
from django.contrib.auth.models import UserManager as DjangoUserManager
|
||||||
from django.contrib.sessions.base_session import AbstractBaseSession
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import Q, QuerySet, options
|
from django.db.models import Q, QuerySet, options
|
||||||
from django.db.models.constants import LOOKUP_SEP
|
from django.db.models.constants import LOOKUP_SEP
|
||||||
@ -648,30 +646,19 @@ class SourceUserMatchingModes(models.TextChoices):
|
|||||||
"""Different modes a source can handle new/returning users"""
|
"""Different modes a source can handle new/returning users"""
|
||||||
|
|
||||||
IDENTIFIER = "identifier", _("Use the source-specific identifier")
|
IDENTIFIER = "identifier", _("Use the source-specific identifier")
|
||||||
EMAIL_LINK = (
|
EMAIL_LINK = "email_link", _(
|
||||||
"email_link",
|
"Link to a user with identical email address. Can have security implications "
|
||||||
_(
|
"when a source doesn't validate email addresses."
|
||||||
"Link to a user with identical email address. Can have security implications "
|
|
||||||
"when a source doesn't validate email addresses."
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
EMAIL_DENY = (
|
EMAIL_DENY = "email_deny", _(
|
||||||
"email_deny",
|
"Use the user's email address, but deny enrollment when the email address already exists."
|
||||||
_(
|
|
||||||
"Use the user's email address, but deny enrollment when the email address already "
|
|
||||||
"exists."
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
USERNAME_LINK = (
|
USERNAME_LINK = "username_link", _(
|
||||||
"username_link",
|
"Link to a user with identical username. Can have security implications "
|
||||||
_(
|
"when a username is used with another source."
|
||||||
"Link to a user with identical username. Can have security implications "
|
|
||||||
"when a username is used with another source."
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
USERNAME_DENY = (
|
USERNAME_DENY = "username_deny", _(
|
||||||
"username_deny",
|
"Use the user's username, but deny enrollment when the username already exists."
|
||||||
_("Use the user's username, but deny enrollment when the username already exists."),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -679,16 +666,12 @@ class SourceGroupMatchingModes(models.TextChoices):
|
|||||||
"""Different modes a source can handle new/returning groups"""
|
"""Different modes a source can handle new/returning groups"""
|
||||||
|
|
||||||
IDENTIFIER = "identifier", _("Use the source-specific identifier")
|
IDENTIFIER = "identifier", _("Use the source-specific identifier")
|
||||||
NAME_LINK = (
|
NAME_LINK = "name_link", _(
|
||||||
"name_link",
|
"Link to a group with identical name. Can have security implications "
|
||||||
_(
|
"when a group name is used with another source."
|
||||||
"Link to a group with identical name. Can have security implications "
|
|
||||||
"when a group name is used with another source."
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
NAME_DENY = (
|
NAME_DENY = "name_deny", _(
|
||||||
"name_deny",
|
"Use the group name, but deny enrollment when the name already exists."
|
||||||
_("Use the group name, but deny enrollment when the name already exists."),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -747,7 +730,8 @@ class Source(ManagedModel, SerializerModel, PolicyBindingModel):
|
|||||||
choices=SourceGroupMatchingModes.choices,
|
choices=SourceGroupMatchingModes.choices,
|
||||||
default=SourceGroupMatchingModes.IDENTIFIER,
|
default=SourceGroupMatchingModes.IDENTIFIER,
|
||||||
help_text=_(
|
help_text=_(
|
||||||
"How the source determines if an existing group should be used or a new group created."
|
"How the source determines if an existing group should be used or "
|
||||||
|
"a new group created."
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -1028,75 +1012,45 @@ class PropertyMapping(SerializerModel, ManagedModel):
|
|||||||
verbose_name_plural = _("Property Mappings")
|
verbose_name_plural = _("Property Mappings")
|
||||||
|
|
||||||
|
|
||||||
class Session(ExpiringModel, AbstractBaseSession):
|
class AuthenticatedSession(ExpiringModel):
|
||||||
"""User session with extra fields for fast access"""
|
"""Additional session class for authenticated users. Augments the standard django session
|
||||||
|
to achieve the following:
|
||||||
|
- Make it queryable by user
|
||||||
|
- Have a direct connection to user objects
|
||||||
|
- Allow users to view their own sessions and terminate them
|
||||||
|
- Save structured and well-defined information.
|
||||||
|
"""
|
||||||
|
|
||||||
# Remove upstream field because we're using our own ExpiringModel
|
uuid = models.UUIDField(default=uuid4, primary_key=True)
|
||||||
expire_date = None
|
|
||||||
session_data = models.BinaryField(_("session data"))
|
|
||||||
|
|
||||||
# Keep in sync with Session.Keys
|
session_key = models.CharField(max_length=40)
|
||||||
last_ip = models.GenericIPAddressField()
|
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||||
|
|
||||||
|
last_ip = models.TextField()
|
||||||
last_user_agent = models.TextField(blank=True)
|
last_user_agent = models.TextField(blank=True)
|
||||||
last_used = models.DateTimeField(auto_now=True)
|
last_used = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name = _("Session")
|
|
||||||
verbose_name_plural = _("Sessions")
|
|
||||||
indexes = ExpiringModel.Meta.indexes + [
|
|
||||||
models.Index(fields=["expires", "session_key"]),
|
|
||||||
]
|
|
||||||
default_permissions = []
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.session_key
|
|
||||||
|
|
||||||
class Keys(StrEnum):
|
|
||||||
"""
|
|
||||||
Keys to be set with the session interface for the fields above to be updated.
|
|
||||||
|
|
||||||
If a field is added here that needs to be initialized when the session is initialized,
|
|
||||||
it must also be reflected in authentik.root.middleware.SessionMiddleware.process_request
|
|
||||||
and in authentik.core.sessions.SessionStore.__init__
|
|
||||||
"""
|
|
||||||
|
|
||||||
LAST_IP = "last_ip"
|
|
||||||
LAST_USER_AGENT = "last_user_agent"
|
|
||||||
LAST_USED = "last_used"
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_session_store_class(cls):
|
|
||||||
from authentik.core.sessions import SessionStore
|
|
||||||
|
|
||||||
return SessionStore
|
|
||||||
|
|
||||||
def get_decoded(self):
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
|
|
||||||
class AuthenticatedSession(SerializerModel):
|
|
||||||
session = models.OneToOneField(Session, on_delete=models.CASCADE, primary_key=True)
|
|
||||||
# We use the session as primary key, but we need the API to be able to reference
|
|
||||||
# this object uniquely without exposing the session key
|
|
||||||
uuid = models.UUIDField(default=uuid4, unique=True)
|
|
||||||
|
|
||||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("Authenticated Session")
|
verbose_name = _("Authenticated Session")
|
||||||
verbose_name_plural = _("Authenticated Sessions")
|
verbose_name_plural = _("Authenticated Sessions")
|
||||||
|
indexes = ExpiringModel.Meta.indexes + [
|
||||||
|
models.Index(fields=["session_key"]),
|
||||||
|
]
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return f"Authenticated Session {str(self.pk)[:10]}"
|
return f"Authenticated Session {self.session_key[:10]}"
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_request(request: HttpRequest, user: User) -> Optional["AuthenticatedSession"]:
|
def from_request(request: HttpRequest, user: User) -> Optional["AuthenticatedSession"]:
|
||||||
"""Create a new session from a http request"""
|
"""Create a new session from a http request"""
|
||||||
if not hasattr(request, "session") or not request.session.exists(
|
from authentik.root.middleware import ClientIPMiddleware
|
||||||
request.session.session_key
|
|
||||||
):
|
if not hasattr(request, "session") or not request.session.session_key:
|
||||||
return None
|
return None
|
||||||
return AuthenticatedSession(
|
return AuthenticatedSession(
|
||||||
session=Session.objects.filter(session_key=request.session.session_key).first(),
|
session_key=request.session.session_key,
|
||||||
user=user,
|
user=user,
|
||||||
|
last_ip=ClientIPMiddleware.get_client_ip(request),
|
||||||
|
last_user_agent=request.META.get("HTTP_USER_AGENT", ""),
|
||||||
|
expires=request.session.get_expiry_date(),
|
||||||
)
|
)
|
||||||
|
@ -1,168 +0,0 @@
|
|||||||
"""authentik sessions engine"""
|
|
||||||
|
|
||||||
import pickle # nosec
|
|
||||||
|
|
||||||
from django.contrib.auth import BACKEND_SESSION_KEY, HASH_SESSION_KEY, SESSION_KEY
|
|
||||||
from django.contrib.sessions.backends.db import SessionStore as SessionBase
|
|
||||||
from django.core.exceptions import SuspiciousOperation
|
|
||||||
from django.utils import timezone
|
|
||||||
from django.utils.functional import cached_property
|
|
||||||
from structlog.stdlib import get_logger
|
|
||||||
|
|
||||||
from authentik.root.middleware import ClientIPMiddleware
|
|
||||||
|
|
||||||
LOGGER = get_logger()
|
|
||||||
|
|
||||||
|
|
||||||
class SessionStore(SessionBase):
|
|
||||||
def __init__(self, session_key=None, last_ip=None, last_user_agent=""):
|
|
||||||
super().__init__(session_key)
|
|
||||||
self._create_kwargs = {
|
|
||||||
"last_ip": last_ip or ClientIPMiddleware.default_ip,
|
|
||||||
"last_user_agent": last_user_agent,
|
|
||||||
}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_model_class(cls):
|
|
||||||
from authentik.core.models import Session
|
|
||||||
|
|
||||||
return Session
|
|
||||||
|
|
||||||
@cached_property
|
|
||||||
def model_fields(self):
|
|
||||||
return [k.value for k in self.model.Keys]
|
|
||||||
|
|
||||||
def _get_session_from_db(self):
|
|
||||||
try:
|
|
||||||
return (
|
|
||||||
self.model.objects.select_related(
|
|
||||||
"authenticatedsession",
|
|
||||||
"authenticatedsession__user",
|
|
||||||
)
|
|
||||||
.prefetch_related(
|
|
||||||
"authenticatedsession__user__groups",
|
|
||||||
"authenticatedsession__user__user_permissions",
|
|
||||||
)
|
|
||||||
.get(
|
|
||||||
session_key=self.session_key,
|
|
||||||
expires__gt=timezone.now(),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
except (self.model.DoesNotExist, SuspiciousOperation) as exc:
|
|
||||||
if isinstance(exc, SuspiciousOperation):
|
|
||||||
LOGGER.warning(str(exc))
|
|
||||||
self._session_key = None
|
|
||||||
|
|
||||||
async def _aget_session_from_db(self):
|
|
||||||
try:
|
|
||||||
return (
|
|
||||||
await self.model.objects.select_related(
|
|
||||||
"authenticatedsession",
|
|
||||||
"authenticatedsession__user",
|
|
||||||
)
|
|
||||||
.prefetch_related(
|
|
||||||
"authenticatedsession__user__groups",
|
|
||||||
"authenticatedsession__user__user_permissions",
|
|
||||||
)
|
|
||||||
.aget(
|
|
||||||
session_key=self.session_key,
|
|
||||||
expires__gt=timezone.now(),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
except (self.model.DoesNotExist, SuspiciousOperation) as exc:
|
|
||||||
if isinstance(exc, SuspiciousOperation):
|
|
||||||
LOGGER.warning(str(exc))
|
|
||||||
self._session_key = None
|
|
||||||
|
|
||||||
def encode(self, session_dict):
|
|
||||||
return pickle.dumps(session_dict, protocol=pickle.HIGHEST_PROTOCOL)
|
|
||||||
|
|
||||||
def decode(self, session_data):
|
|
||||||
try:
|
|
||||||
return pickle.loads(session_data) # nosec
|
|
||||||
except pickle.PickleError:
|
|
||||||
# ValueError, unpickling exceptions. If any of these happen, just return an empty
|
|
||||||
# dictionary (an empty session)
|
|
||||||
pass
|
|
||||||
return {}
|
|
||||||
|
|
||||||
def load(self):
|
|
||||||
s = self._get_session_from_db()
|
|
||||||
if s:
|
|
||||||
return {
|
|
||||||
"authenticatedsession": getattr(s, "authenticatedsession", None),
|
|
||||||
**{k: getattr(s, k) for k in self.model_fields},
|
|
||||||
**self.decode(s.session_data),
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
return {}
|
|
||||||
|
|
||||||
async def aload(self):
|
|
||||||
s = await self._aget_session_from_db()
|
|
||||||
if s:
|
|
||||||
return {
|
|
||||||
"authenticatedsession": getattr(s, "authenticatedsession", None),
|
|
||||||
**{k: getattr(s, k) for k in self.model_fields},
|
|
||||||
**self.decode(s.session_data),
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
return {}
|
|
||||||
|
|
||||||
def create_model_instance(self, data):
|
|
||||||
args = {
|
|
||||||
"session_key": self._get_or_create_session_key(),
|
|
||||||
"expires": self.get_expiry_date(),
|
|
||||||
"session_data": {},
|
|
||||||
**self._create_kwargs,
|
|
||||||
}
|
|
||||||
for k, v in data.items():
|
|
||||||
# Don't save:
|
|
||||||
# - unused auth data
|
|
||||||
# - related models
|
|
||||||
if k in [SESSION_KEY, BACKEND_SESSION_KEY, HASH_SESSION_KEY, "authenticatedsession"]:
|
|
||||||
pass
|
|
||||||
elif k in self.model_fields:
|
|
||||||
args[k] = v
|
|
||||||
else:
|
|
||||||
args["session_data"][k] = v
|
|
||||||
args["session_data"] = self.encode(args["session_data"])
|
|
||||||
return self.model(**args)
|
|
||||||
|
|
||||||
async def acreate_model_instance(self, data):
|
|
||||||
args = {
|
|
||||||
"session_key": await self._aget_or_create_session_key(),
|
|
||||||
"expires": await self.aget_expiry_date(),
|
|
||||||
"session_data": {},
|
|
||||||
**self._create_kwargs,
|
|
||||||
}
|
|
||||||
for k, v in data.items():
|
|
||||||
# Don't save:
|
|
||||||
# - unused auth data
|
|
||||||
# - related models
|
|
||||||
if k in [SESSION_KEY, BACKEND_SESSION_KEY, HASH_SESSION_KEY, "authenticatedsession"]:
|
|
||||||
pass
|
|
||||||
elif k in self.model_fields:
|
|
||||||
args[k] = v
|
|
||||||
else:
|
|
||||||
args["session_data"][k] = v
|
|
||||||
args["session_data"] = self.encode(args["session_data"])
|
|
||||||
return self.model(**args)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def clear_expired(cls):
|
|
||||||
cls.get_model_class().objects.filter(expires__lt=timezone.now()).delete()
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
async def aclear_expired(cls):
|
|
||||||
await cls.get_model_class().objects.filter(expires__lt=timezone.now()).adelete()
|
|
||||||
|
|
||||||
def cycle_key(self):
|
|
||||||
data = self._session
|
|
||||||
key = self.session_key
|
|
||||||
self.create()
|
|
||||||
self._session_cache = data
|
|
||||||
if key:
|
|
||||||
self.delete(key)
|
|
||||||
if (authenticated_session := data.get("authenticatedsession")) is not None:
|
|
||||||
authenticated_session.session_id = self.session_key
|
|
||||||
authenticated_session.save(force_insert=True)
|
|
@ -1,10 +1,14 @@
|
|||||||
"""authentik core signals"""
|
"""authentik core signals"""
|
||||||
|
|
||||||
from django.contrib.auth.signals import user_logged_in
|
from importlib import import_module
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.contrib.auth.signals import user_logged_in, user_logged_out
|
||||||
|
from django.contrib.sessions.backends.base import SessionBase
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.core.signals import Signal
|
from django.core.signals import Signal
|
||||||
from django.db.models import Model
|
from django.db.models import Model
|
||||||
from django.db.models.signals import post_delete, post_save, pre_save
|
from django.db.models.signals import post_save, pre_delete, pre_save
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.http.request import HttpRequest
|
from django.http.request import HttpRequest
|
||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
@ -14,7 +18,6 @@ from authentik.core.models import (
|
|||||||
AuthenticatedSession,
|
AuthenticatedSession,
|
||||||
BackchannelProvider,
|
BackchannelProvider,
|
||||||
ExpiringModel,
|
ExpiringModel,
|
||||||
Session,
|
|
||||||
User,
|
User,
|
||||||
default_token_duration,
|
default_token_duration,
|
||||||
)
|
)
|
||||||
@ -25,6 +28,7 @@ password_changed = Signal()
|
|||||||
login_failed = Signal()
|
login_failed = Signal()
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
SessionStore: SessionBase = import_module(settings.SESSION_ENGINE).SessionStore
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_save, sender=Application)
|
@receiver(post_save, sender=Application)
|
||||||
@ -49,10 +53,18 @@ def user_logged_in_session(sender, request: HttpRequest, user: User, **_):
|
|||||||
session.save()
|
session.save()
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_delete, sender=AuthenticatedSession)
|
@receiver(user_logged_out)
|
||||||
|
def user_logged_out_session(sender, request: HttpRequest, user: User, **_):
|
||||||
|
"""Delete AuthenticatedSession if it exists"""
|
||||||
|
if not request.session or not request.session.session_key:
|
||||||
|
return
|
||||||
|
AuthenticatedSession.objects.filter(session_key=request.session.session_key).delete()
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(pre_delete, sender=AuthenticatedSession)
|
||||||
def authenticated_session_delete(sender: type[Model], instance: "AuthenticatedSession", **_):
|
def authenticated_session_delete(sender: type[Model], instance: "AuthenticatedSession", **_):
|
||||||
"""Delete session when authenticated session is deleted"""
|
"""Delete session when authenticated session is deleted"""
|
||||||
Session.objects.filter(session_key=instance.pk).delete()
|
SessionStore(instance.session_key).delete()
|
||||||
|
|
||||||
|
|
||||||
@receiver(pre_save)
|
@receiver(pre_save)
|
||||||
|
@ -2,16 +2,22 @@
|
|||||||
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
from django.conf import ImproperlyConfigured
|
||||||
|
from django.contrib.sessions.backends.cache import KEY_PREFIX
|
||||||
|
from django.contrib.sessions.backends.db import SessionStore as DBSessionStore
|
||||||
|
from django.core.cache import cache
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
from authentik.core.models import (
|
from authentik.core.models import (
|
||||||
USER_ATTRIBUTE_EXPIRES,
|
USER_ATTRIBUTE_EXPIRES,
|
||||||
USER_ATTRIBUTE_GENERATED,
|
USER_ATTRIBUTE_GENERATED,
|
||||||
|
AuthenticatedSession,
|
||||||
ExpiringModel,
|
ExpiringModel,
|
||||||
User,
|
User,
|
||||||
)
|
)
|
||||||
from authentik.events.system_tasks import SystemTask, TaskStatus, prefill_task
|
from authentik.events.system_tasks import SystemTask, TaskStatus, prefill_task
|
||||||
|
from authentik.lib.config import CONFIG
|
||||||
from authentik.root.celery import CELERY_APP
|
from authentik.root.celery import CELERY_APP
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
@ -32,6 +38,40 @@ def clean_expired_models(self: SystemTask):
|
|||||||
obj.expire_action()
|
obj.expire_action()
|
||||||
LOGGER.debug("Expired models", model=cls, amount=amount)
|
LOGGER.debug("Expired models", model=cls, amount=amount)
|
||||||
messages.append(f"Expired {amount} {cls._meta.verbose_name_plural}")
|
messages.append(f"Expired {amount} {cls._meta.verbose_name_plural}")
|
||||||
|
# Special case
|
||||||
|
amount = 0
|
||||||
|
|
||||||
|
for session in AuthenticatedSession.objects.all():
|
||||||
|
match CONFIG.get("session_storage", "cache"):
|
||||||
|
case "cache":
|
||||||
|
cache_key = f"{KEY_PREFIX}{session.session_key}"
|
||||||
|
value = None
|
||||||
|
try:
|
||||||
|
value = cache.get(cache_key)
|
||||||
|
|
||||||
|
except Exception as exc:
|
||||||
|
LOGGER.debug("Failed to get session from cache", exc=exc)
|
||||||
|
if not value:
|
||||||
|
session.delete()
|
||||||
|
amount += 1
|
||||||
|
case "db":
|
||||||
|
if not (
|
||||||
|
DBSessionStore.get_model_class()
|
||||||
|
.objects.filter(session_key=session.session_key, expire_date__gt=now())
|
||||||
|
.exists()
|
||||||
|
):
|
||||||
|
session.delete()
|
||||||
|
amount += 1
|
||||||
|
case _:
|
||||||
|
# Should never happen, as we check for other values in authentik/root/settings.py
|
||||||
|
raise ImproperlyConfigured(
|
||||||
|
"Invalid session_storage setting, allowed values are db and cache"
|
||||||
|
)
|
||||||
|
if CONFIG.get("session_storage", "cache") == "db":
|
||||||
|
DBSessionStore.clear_expired()
|
||||||
|
LOGGER.debug("Expired sessions", model=AuthenticatedSession, amount=amount)
|
||||||
|
|
||||||
|
messages.append(f"Expired {amount} {AuthenticatedSession._meta.verbose_name_plural}")
|
||||||
self.set_status(TaskStatus.SUCCESSFUL, *messages)
|
self.set_status(TaskStatus.SUCCESSFUL, *messages)
|
||||||
|
|
||||||
|
|
||||||
|
@ -2,22 +2,20 @@
|
|||||||
{% 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,
|
||||||
@ -28,7 +26,6 @@
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
});
|
});
|
||||||
{% endif %}
|
|
||||||
</script>
|
</script>
|
||||||
|
@ -2,79 +2,31 @@
|
|||||||
{% 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 #}
|
||||||
{% comment %}
|
<meta name="darkreader-lock">
|
||||||
Darkreader breaks the site regardless of theme as its not compatible with webcomponents, and we
|
<title>{% block title %}{% trans title|default:brand.branding_title %}{% endblock %}</title>
|
||||||
default to a dark theme based on preferred colour-scheme
|
<link rel="icon" href="{{ brand.branding_favicon_url }}">
|
||||||
{% endcomment %}
|
<link rel="shortcut icon" href="{{ brand.branding_favicon_url }}">
|
||||||
|
{% block head_before %}
|
||||||
<meta name="darkreader-lock" />
|
{% endblock %}
|
||||||
|
<link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}">
|
||||||
<title>{% block title %}{% trans title|default:brand.branding_title %}{% endblock %}</title>
|
<style>{{ brand.branding_custom_css }}</style>
|
||||||
|
<script src="{% versioned_script 'dist/poly-%v.js' %}" type="module"></script>
|
||||||
<link rel="icon" href="{{ brand.branding_favicon_url }}" />
|
<script src="{% versioned_script 'dist/standalone/loading/index-%v.js' %}" type="module"></script>
|
||||||
<link rel="shortcut icon" href="{{ brand.branding_favicon_url }}" />
|
{% block head %}
|
||||||
|
{% endblock %}
|
||||||
{% block head_before %}
|
<meta name="sentry-trace" content="{{ sentry_trace }}" />
|
||||||
{% endblock %}
|
</head>
|
||||||
|
<body>
|
||||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}" />
|
{% block body %}
|
||||||
|
{% endblock %}
|
||||||
<style data-test-id="color-scheme">
|
{% block scripts %}
|
||||||
@media (prefers-color-scheme: dark) {
|
{% endblock %}
|
||||||
:root {
|
</body>
|
||||||
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,16 +4,14 @@
|
|||||||
|
|
||||||
{% 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="#18191a" media="(prefers-color-scheme: dark)" />
|
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">
|
||||||
<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,14 +13,9 @@
|
|||||||
|
|
||||||
{% 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">
|
||||||
<a
|
{% trans 'Go home' %}
|
||||||
id="ak-back-home"
|
</a>
|
||||||
href="{% url 'authentik_core:root-redirect' %}"
|
|
||||||
class="pf-c-button pf-m-primary"
|
|
||||||
>
|
|
||||||
{% trans 'Go home' %}
|
|
||||||
</a>
|
|
||||||
</form>
|
</form>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -4,17 +4,14 @@
|
|||||||
|
|
||||||
{% 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: light)" />
|
<meta name="theme-color" content="#1c1e21" media="(prefers-color-scheme: dark)">
|
||||||
<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,82 +5,78 @@
|
|||||||
|
|
||||||
{% 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
|
<link rel="stylesheet" type="text/css" href="{% static 'dist/theme-dark.css' %}" media="(prefers-color-scheme: dark)">
|
||||||
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 data-test-id="base-full-root-styles">
|
<style>
|
||||||
: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>
|
<div class="pf-c-background-image">
|
||||||
|
</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,17 +1,9 @@
|
|||||||
"""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):
|
||||||
@ -22,14 +14,3 @@ 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)
|
|
||||||
|
@ -5,7 +5,7 @@ from json import loads
|
|||||||
from django.urls.base import reverse
|
from django.urls.base import reverse
|
||||||
from rest_framework.test import APITestCase
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
from authentik.core.models import AuthenticatedSession, Session, User
|
from authentik.core.models import User
|
||||||
from authentik.core.tests.utils import create_test_admin_user
|
from authentik.core.tests.utils import create_test_admin_user
|
||||||
|
|
||||||
|
|
||||||
@ -30,18 +30,3 @@ class TestAuthenticatedSessionsAPI(APITestCase):
|
|||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
body = loads(response.content.decode())
|
body = loads(response.content.decode())
|
||||||
self.assertEqual(body["pagination"]["count"], 1)
|
self.assertEqual(body["pagination"]["count"], 1)
|
||||||
|
|
||||||
def test_delete(self):
|
|
||||||
"""Test deletion"""
|
|
||||||
self.client.force_login(self.user)
|
|
||||||
self.assertEqual(AuthenticatedSession.objects.all().count(), 1)
|
|
||||||
self.assertEqual(Session.objects.all().count(), 1)
|
|
||||||
response = self.client.delete(
|
|
||||||
reverse(
|
|
||||||
"authentik_api:authenticatedsession-detail",
|
|
||||||
kwargs={"uuid": AuthenticatedSession.objects.first().uuid},
|
|
||||||
)
|
|
||||||
)
|
|
||||||
self.assertEqual(response.status_code, 204)
|
|
||||||
self.assertEqual(AuthenticatedSession.objects.all().count(), 0)
|
|
||||||
self.assertEqual(Session.objects.all().count(), 0)
|
|
||||||
|
@ -3,6 +3,8 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from json import loads
|
from json import loads
|
||||||
|
|
||||||
|
from django.contrib.sessions.backends.cache import KEY_PREFIX
|
||||||
|
from django.core.cache import cache
|
||||||
from django.urls.base import reverse
|
from django.urls.base import reverse
|
||||||
from rest_framework.test import APITestCase
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
@ -10,7 +12,6 @@ from authentik.brands.models import Brand
|
|||||||
from authentik.core.models import (
|
from authentik.core.models import (
|
||||||
USER_ATTRIBUTE_TOKEN_EXPIRING,
|
USER_ATTRIBUTE_TOKEN_EXPIRING,
|
||||||
AuthenticatedSession,
|
AuthenticatedSession,
|
||||||
Session,
|
|
||||||
Token,
|
Token,
|
||||||
User,
|
User,
|
||||||
UserTypes,
|
UserTypes,
|
||||||
@ -380,15 +381,12 @@ class TestUsersAPI(APITestCase):
|
|||||||
"""Ensure sessions are deleted when a user is deactivated"""
|
"""Ensure sessions are deleted when a user is deactivated"""
|
||||||
user = create_test_admin_user()
|
user = create_test_admin_user()
|
||||||
session_id = generate_id()
|
session_id = generate_id()
|
||||||
session = Session.objects.create(
|
|
||||||
session_key=session_id,
|
|
||||||
last_ip="255.255.255.255",
|
|
||||||
last_user_agent="",
|
|
||||||
)
|
|
||||||
AuthenticatedSession.objects.create(
|
AuthenticatedSession.objects.create(
|
||||||
session=session,
|
|
||||||
user=user,
|
user=user,
|
||||||
|
session_key=session_id,
|
||||||
|
last_ip="",
|
||||||
)
|
)
|
||||||
|
cache.set(KEY_PREFIX + session_id, "foo")
|
||||||
|
|
||||||
self.client.force_login(self.admin)
|
self.client.force_login(self.admin)
|
||||||
response = self.client.patch(
|
response = self.client.patch(
|
||||||
@ -399,7 +397,5 @@ class TestUsersAPI(APITestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
self.assertFalse(Session.objects.filter(session_key=session_id).exists())
|
self.assertIsNone(cache.get(KEY_PREFIX + session_id))
|
||||||
self.assertFalse(
|
self.assertFalse(AuthenticatedSession.objects.filter(session_key=session_id).exists())
|
||||||
AuthenticatedSession.objects.filter(session__session_key=session_id).exists()
|
|
||||||
)
|
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
"""authentik URL Configuration"""
|
"""authentik URL Configuration"""
|
||||||
|
|
||||||
|
from channels.auth import AuthMiddleware
|
||||||
|
from channels.sessions import CookieMiddleware
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
@ -27,7 +29,7 @@ from authentik.core.views.interface import (
|
|||||||
RootRedirectView,
|
RootRedirectView,
|
||||||
)
|
)
|
||||||
from authentik.flows.views.interface import FlowInterfaceView
|
from authentik.flows.views.interface import FlowInterfaceView
|
||||||
from authentik.root.asgi_middleware import AuthMiddlewareStack
|
from authentik.root.asgi_middleware import SessionMiddleware
|
||||||
from authentik.root.messages.consumer import MessageConsumer
|
from authentik.root.messages.consumer import MessageConsumer
|
||||||
from authentik.root.middleware import ChannelsLoggingMiddleware
|
from authentik.root.middleware import ChannelsLoggingMiddleware
|
||||||
|
|
||||||
@ -97,7 +99,9 @@ api_urlpatterns = [
|
|||||||
websocket_urlpatterns = [
|
websocket_urlpatterns = [
|
||||||
path(
|
path(
|
||||||
"ws/client/",
|
"ws/client/",
|
||||||
ChannelsLoggingMiddleware(AuthMiddlewareStack(MessageConsumer.as_asgi())),
|
ChannelsLoggingMiddleware(
|
||||||
|
CookieMiddleware(SessionMiddleware(AuthMiddleware(MessageConsumer.as_asgi())))
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -102,7 +102,7 @@ def ssf_user_session_delete_session_revoked(sender, instance: AuthenticatedSessi
|
|||||||
"format": "complex",
|
"format": "complex",
|
||||||
"session": {
|
"session": {
|
||||||
"format": "opaque",
|
"format": "opaque",
|
||||||
"id": sha256(instance.session.session_key.encode("ascii")).hexdigest(),
|
"id": sha256(instance.session_key.encode("ascii")).hexdigest(),
|
||||||
},
|
},
|
||||||
"user": {
|
"user": {
|
||||||
"format": "email",
|
"format": "email",
|
||||||
|
@ -4,9 +4,10 @@ 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 ModelSerializer, PassiveSerializer
|
from authentik.core.api.utils import 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,
|
||||||
|
@ -59,7 +59,7 @@ def get_login_event(request_or_session: HttpRequest | AuthenticatedSession | Non
|
|||||||
session = request_or_session.session
|
session = request_or_session.session
|
||||||
if isinstance(request_or_session, AuthenticatedSession):
|
if isinstance(request_or_session, AuthenticatedSession):
|
||||||
SessionStore = _session_engine.SessionStore
|
SessionStore = _session_engine.SessionStore
|
||||||
session = SessionStore(request_or_session.session.session_key)
|
session = SessionStore(request_or_session.session_key)
|
||||||
return session.get(SESSION_LOGIN_EVENT, None)
|
return session.get(SESSION_LOGIN_EVENT, None)
|
||||||
|
|
||||||
|
|
||||||
|
@ -2,52 +2,53 @@
|
|||||||
{% 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>
|
<main class="form-signin w-100 m-auto" id="flow-sfe-container">
|
||||||
<span class="mt-3 mb-0 text-muted text-center">{% trans 'Powered by authentik' %}</span>
|
</main>
|
||||||
</div>
|
<span class="mt-3 mb-0 text-muted text-center">{% trans 'Powered by authentik' %}</span>
|
||||||
<script src="{% static 'dist/sfe/index.js' %}"></script>
|
</div>
|
||||||
</body>
|
<script src="{% static 'dist/sfe/index.js' %}"></script>
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -1,40 +1,34 @@
|
|||||||
{% 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>
|
<script>ShadyDOM = { force: !navigator.webdriver };</script>
|
||||||
ShadyDOM = { force: !navigator.webdriver };
|
|
||||||
</script>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% include "base/header_js.html" %}
|
{% include "base/header_js.html" %}
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
window.authentik.flow = {
|
window.authentik.flow = {
|
||||||
layout: "{{ flow.layout }}",
|
"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>
|
||||||
<style data-test-id="flow-root-styles">
|
:root {
|
||||||
: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,14 +356,6 @@ 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",
|
||||||
@ -377,7 +369,6 @@ 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,7 +21,6 @@ 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
|
||||||
|
@ -18,7 +18,7 @@ from sentry_sdk import start_span
|
|||||||
from sentry_sdk.tracing import Span
|
from sentry_sdk.tracing import Span
|
||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
from authentik.core.models import User
|
from authentik.core.models import AuthenticatedSession, User
|
||||||
from authentik.events.models import Event
|
from authentik.events.models import Event
|
||||||
from authentik.lib.expression.exceptions import ControlFlowException
|
from authentik.lib.expression.exceptions import ControlFlowException
|
||||||
from authentik.lib.utils.http import get_http_session
|
from authentik.lib.utils.http import get_http_session
|
||||||
@ -203,7 +203,9 @@ class BaseEvaluator:
|
|||||||
provider = OAuth2Provider.objects.get(name=provider)
|
provider = OAuth2Provider.objects.get(name=provider)
|
||||||
session = None
|
session = None
|
||||||
if hasattr(request, "session") and request.session.session_key:
|
if hasattr(request, "session") and request.session.session_key:
|
||||||
session = request.session["authenticatedsession"]
|
session = AuthenticatedSession.objects.filter(
|
||||||
|
session_key=request.session.session_key
|
||||||
|
).first()
|
||||||
access_token = AccessToken(
|
access_token = AccessToken(
|
||||||
provider=provider,
|
provider=provider,
|
||||||
user=user,
|
user=user,
|
||||||
|
@ -217,7 +217,6 @@ 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",
|
||||||
@ -268,7 +267,6 @@ 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",
|
||||||
@ -287,7 +285,6 @@ 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",
|
||||||
@ -336,7 +333,6 @@ 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",
|
||||||
@ -355,7 +351,6 @@ 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",
|
||||||
@ -399,7 +394,6 @@ 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",
|
||||||
@ -418,7 +412,6 @@ 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",
|
||||||
@ -458,7 +451,6 @@ 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",
|
||||||
@ -477,7 +469,6 @@ 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",
|
||||||
@ -493,87 +484,3 @@ 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,9 +66,7 @@ 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)
|
||||||
|
|
||||||
static_passing = any(r.passing for r in static_results) if static_results else True
|
passing = any(r.passing for r in static_results) and all(r.passing for r in dynamic_results)
|
||||||
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]
|
||||||
)
|
)
|
||||||
@ -115,19 +113,13 @@ 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,
|
action=EventAction.LOGIN, user__pk=request.user.pk, context__geo__isnull=False
|
||||||
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
|
||||||
@ -150,8 +142,7 @@ 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."))
|
||||||
result = True
|
return PolicyResult(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_failing(self):
|
def test_history_impossible_travel(self):
|
||||||
"""Test history checks"""
|
"""Test history checks"""
|
||||||
Event.objects.create(
|
Event.objects.create(
|
||||||
action=EventAction.LOGIN,
|
action=EventAction.LOGIN,
|
||||||
@ -181,24 +181,6 @@ 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(
|
||||||
@ -213,18 +195,3 @@ 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)
|
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
from django.contrib.auth.signals import user_logged_in
|
from django.contrib.auth.signals import user_logged_in
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
|
from django.db.models import F
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
@ -12,29 +13,20 @@ from authentik.events.context_processors.geoip import GEOIP_CONTEXT_PROCESSOR
|
|||||||
from authentik.policies.reputation.models import Reputation, reputation_expiry
|
from authentik.policies.reputation.models import Reputation, reputation_expiry
|
||||||
from authentik.root.middleware import ClientIPMiddleware
|
from authentik.root.middleware import ClientIPMiddleware
|
||||||
from authentik.stages.identification.signals import identification_failed
|
from authentik.stages.identification.signals import identification_failed
|
||||||
from authentik.tenants.utils import get_current_tenant
|
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
|
||||||
|
|
||||||
def clamp(value, min, max):
|
|
||||||
return sorted([min, value, max])[1]
|
|
||||||
|
|
||||||
|
|
||||||
def update_score(request: HttpRequest, identifier: str, amount: int):
|
def update_score(request: HttpRequest, identifier: str, amount: int):
|
||||||
"""Update score for IP and User"""
|
"""Update score for IP and User"""
|
||||||
remote_ip = ClientIPMiddleware.get_client_ip(request)
|
remote_ip = ClientIPMiddleware.get_client_ip(request)
|
||||||
tenant = get_current_tenant()
|
|
||||||
new_score = clamp(amount, tenant.reputation_lower_limit, tenant.reputation_upper_limit)
|
|
||||||
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
reputation, created = Reputation.objects.select_for_update().get_or_create(
|
reputation, created = Reputation.objects.select_for_update().get_or_create(
|
||||||
ip=remote_ip,
|
ip=remote_ip,
|
||||||
identifier=identifier,
|
identifier=identifier,
|
||||||
defaults={
|
defaults={
|
||||||
"score": clamp(
|
"score": amount,
|
||||||
amount, tenant.reputation_lower_limit, tenant.reputation_upper_limit
|
|
||||||
),
|
|
||||||
"ip_geo_data": GEOIP_CONTEXT_PROCESSOR.city_dict(remote_ip) or {},
|
"ip_geo_data": GEOIP_CONTEXT_PROCESSOR.city_dict(remote_ip) or {},
|
||||||
"ip_asn_data": ASN_CONTEXT_PROCESSOR.asn_dict(remote_ip) or {},
|
"ip_asn_data": ASN_CONTEXT_PROCESSOR.asn_dict(remote_ip) or {},
|
||||||
"expires": reputation_expiry(),
|
"expires": reputation_expiry(),
|
||||||
@ -42,15 +34,9 @@ def update_score(request: HttpRequest, identifier: str, amount: int):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if not created:
|
if not created:
|
||||||
new_score = clamp(
|
reputation.score = F("score") + amount
|
||||||
reputation.score + amount,
|
|
||||||
tenant.reputation_lower_limit,
|
|
||||||
tenant.reputation_upper_limit,
|
|
||||||
)
|
|
||||||
reputation.score = new_score
|
|
||||||
reputation.save()
|
reputation.save()
|
||||||
|
LOGGER.info("Updated score", amount=amount, for_user=identifier, for_ip=remote_ip)
|
||||||
LOGGER.info("Updated score", amount=new_score, for_user=identifier, for_ip=remote_ip)
|
|
||||||
|
|
||||||
|
|
||||||
@receiver(login_failed)
|
@receiver(login_failed)
|
||||||
|
@ -6,11 +6,9 @@ from authentik.core.models import User
|
|||||||
from authentik.lib.generators import generate_id
|
from authentik.lib.generators import generate_id
|
||||||
from authentik.policies.reputation.api import ReputationPolicySerializer
|
from authentik.policies.reputation.api import ReputationPolicySerializer
|
||||||
from authentik.policies.reputation.models import Reputation, ReputationPolicy
|
from authentik.policies.reputation.models import Reputation, ReputationPolicy
|
||||||
from authentik.policies.reputation.signals import update_score
|
|
||||||
from authentik.policies.types import PolicyRequest
|
from authentik.policies.types import PolicyRequest
|
||||||
from authentik.stages.password import BACKEND_INBUILT
|
from authentik.stages.password import BACKEND_INBUILT
|
||||||
from authentik.stages.password.stage import authenticate
|
from authentik.stages.password.stage import authenticate
|
||||||
from authentik.tenants.models import DEFAULT_REPUTATION_LOWER_LIMIT, DEFAULT_REPUTATION_UPPER_LIMIT
|
|
||||||
|
|
||||||
|
|
||||||
class TestReputationPolicy(TestCase):
|
class TestReputationPolicy(TestCase):
|
||||||
@ -19,48 +17,36 @@ class TestReputationPolicy(TestCase):
|
|||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.request_factory = RequestFactory()
|
self.request_factory = RequestFactory()
|
||||||
self.request = self.request_factory.get("/")
|
self.request = self.request_factory.get("/")
|
||||||
self.ip = "127.0.0.1"
|
self.test_ip = "127.0.0.1"
|
||||||
self.username = "username"
|
self.test_username = "test"
|
||||||
self.password = generate_id()
|
|
||||||
# We need a user for the one-to-one in userreputation
|
# We need a user for the one-to-one in userreputation
|
||||||
self.user = User.objects.create(username=self.username)
|
self.user = User.objects.create(username=self.test_username)
|
||||||
self.user.set_password(self.password)
|
|
||||||
self.backends = [BACKEND_INBUILT]
|
self.backends = [BACKEND_INBUILT]
|
||||||
|
|
||||||
def test_ip_reputation(self):
|
def test_ip_reputation(self):
|
||||||
"""test IP reputation"""
|
"""test IP reputation"""
|
||||||
# Trigger negative reputation
|
# Trigger negative reputation
|
||||||
authenticate(self.request, self.backends, username=self.username, password=self.username)
|
authenticate(
|
||||||
self.assertEqual(Reputation.objects.get(ip=self.ip).score, -1)
|
self.request, self.backends, username=self.test_username, password=self.test_username
|
||||||
|
)
|
||||||
|
self.assertEqual(Reputation.objects.get(ip=self.test_ip).score, -1)
|
||||||
|
|
||||||
def test_user_reputation(self):
|
def test_user_reputation(self):
|
||||||
"""test User reputation"""
|
"""test User reputation"""
|
||||||
# Trigger negative reputation
|
# Trigger negative reputation
|
||||||
authenticate(self.request, self.backends, username=self.username, password=self.username)
|
authenticate(
|
||||||
self.assertEqual(Reputation.objects.get(identifier=self.username).score, -1)
|
self.request, self.backends, username=self.test_username, password=self.test_username
|
||||||
|
)
|
||||||
|
self.assertEqual(Reputation.objects.get(identifier=self.test_username).score, -1)
|
||||||
|
|
||||||
def test_update_reputation(self):
|
def test_update_reputation(self):
|
||||||
"""test reputation update"""
|
"""test reputation update"""
|
||||||
Reputation.objects.create(identifier=self.username, ip=self.ip, score=4)
|
Reputation.objects.create(identifier=self.test_username, ip=self.test_ip, score=43)
|
||||||
# Trigger negative reputation
|
# Trigger negative reputation
|
||||||
authenticate(self.request, self.backends, username=self.username, password=self.username)
|
authenticate(
|
||||||
self.assertEqual(Reputation.objects.get(identifier=self.username).score, 3)
|
self.request, self.backends, username=self.test_username, password=self.test_username
|
||||||
|
|
||||||
def test_reputation_lower_limit(self):
|
|
||||||
"""test reputation lower limit"""
|
|
||||||
Reputation.objects.create(identifier=self.username, ip=self.ip)
|
|
||||||
update_score(self.request, identifier=self.username, amount=-1000)
|
|
||||||
self.assertEqual(
|
|
||||||
Reputation.objects.get(identifier=self.username).score, DEFAULT_REPUTATION_LOWER_LIMIT
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_reputation_upper_limit(self):
|
|
||||||
"""test reputation upper limit"""
|
|
||||||
Reputation.objects.create(identifier=self.username, ip=self.ip)
|
|
||||||
update_score(self.request, identifier=self.username, amount=1000)
|
|
||||||
self.assertEqual(
|
|
||||||
Reputation.objects.get(identifier=self.username).score, DEFAULT_REPUTATION_UPPER_LIMIT
|
|
||||||
)
|
)
|
||||||
|
self.assertEqual(Reputation.objects.get(identifier=self.test_username).score, 42)
|
||||||
|
|
||||||
def test_policy(self):
|
def test_policy(self):
|
||||||
"""Test Policy"""
|
"""Test Policy"""
|
||||||
|
@ -126,7 +126,7 @@ class IDToken:
|
|||||||
id_token.iat = int(now.timestamp())
|
id_token.iat = int(now.timestamp())
|
||||||
id_token.auth_time = int(token.auth_time.timestamp())
|
id_token.auth_time = int(token.auth_time.timestamp())
|
||||||
if token.session:
|
if token.session:
|
||||||
id_token.sid = hash_session_key(token.session.session.session_key)
|
id_token.sid = hash_session_key(token.session.session_key)
|
||||||
|
|
||||||
# We use the timestamp of the user's last successful login (EventAction.LOGIN) for auth_time
|
# We use the timestamp of the user's last successful login (EventAction.LOGIN) for auth_time
|
||||||
auth_event = get_login_event(token.session)
|
auth_event = get_login_event(token.session)
|
||||||
|
@ -1,116 +0,0 @@
|
|||||||
# Generated by Django 5.0.11 on 2025-01-27 13:00
|
|
||||||
|
|
||||||
from django.db import migrations
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.db import migrations, models
|
|
||||||
from functools import partial
|
|
||||||
|
|
||||||
|
|
||||||
def migrate_sessions(apps, schema_editor, model):
|
|
||||||
Model = apps.get_model("authentik_providers_oauth2", model)
|
|
||||||
AuthenticatedSession = apps.get_model("authentik_core", "AuthenticatedSession")
|
|
||||||
db_alias = schema_editor.connection.alias
|
|
||||||
|
|
||||||
for obj in Model.objects.using(db_alias).all():
|
|
||||||
if not obj.old_session:
|
|
||||||
continue
|
|
||||||
obj.session = (
|
|
||||||
AuthenticatedSession.objects.using(db_alias)
|
|
||||||
.filter(session__session_key=obj.old_session.session_key)
|
|
||||||
.first()
|
|
||||||
)
|
|
||||||
if obj.session:
|
|
||||||
obj.save()
|
|
||||||
else:
|
|
||||||
obj.delete()
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("authentik_providers_oauth2", "0027_accesstoken_authentik_p_expires_9f24a5_idx_and_more"),
|
|
||||||
("authentik_core", "0046_session_and_more"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.RenameField(
|
|
||||||
model_name="accesstoken",
|
|
||||||
old_name="session",
|
|
||||||
new_name="old_session",
|
|
||||||
),
|
|
||||||
migrations.RenameField(
|
|
||||||
model_name="authorizationcode",
|
|
||||||
old_name="session",
|
|
||||||
new_name="old_session",
|
|
||||||
),
|
|
||||||
migrations.RenameField(
|
|
||||||
model_name="devicetoken",
|
|
||||||
old_name="session",
|
|
||||||
new_name="old_session",
|
|
||||||
),
|
|
||||||
migrations.RenameField(
|
|
||||||
model_name="refreshtoken",
|
|
||||||
old_name="session",
|
|
||||||
new_name="old_session",
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="accesstoken",
|
|
||||||
name="session",
|
|
||||||
field=models.ForeignKey(
|
|
||||||
default=None,
|
|
||||||
null=True,
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
to="authentik_core.authenticatedsession",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="authorizationcode",
|
|
||||||
name="session",
|
|
||||||
field=models.ForeignKey(
|
|
||||||
default=None,
|
|
||||||
null=True,
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
to="authentik_core.authenticatedsession",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="devicetoken",
|
|
||||||
name="session",
|
|
||||||
field=models.ForeignKey(
|
|
||||||
default=None,
|
|
||||||
null=True,
|
|
||||||
on_delete=django.db.models.deletion.SET_DEFAULT,
|
|
||||||
to="authentik_core.authenticatedsession",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="refreshtoken",
|
|
||||||
name="session",
|
|
||||||
field=models.ForeignKey(
|
|
||||||
default=None,
|
|
||||||
null=True,
|
|
||||||
on_delete=django.db.models.deletion.SET_DEFAULT,
|
|
||||||
to="authentik_core.authenticatedsession",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.RunPython(code=partial(migrate_sessions, model="AccessToken")),
|
|
||||||
migrations.RunPython(code=partial(migrate_sessions, model="AuthorizationCode")),
|
|
||||||
migrations.RunPython(code=partial(migrate_sessions, model="DeviceToken")),
|
|
||||||
migrations.RunPython(code=partial(migrate_sessions, model="RefreshToken")),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name="accesstoken",
|
|
||||||
name="old_session",
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name="authorizationcode",
|
|
||||||
name="old_session",
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name="devicetoken",
|
|
||||||
name="old_session",
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name="refreshtoken",
|
|
||||||
name="old_session",
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,30 +1,18 @@
|
|||||||
from django.contrib.auth.signals import user_logged_out
|
from django.contrib.auth.signals import user_logged_out
|
||||||
from django.db.models.signals import post_save, pre_delete
|
from django.db.models.signals import post_save
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
|
|
||||||
from authentik.core.models import AuthenticatedSession, User
|
from authentik.core.models import User
|
||||||
from authentik.providers.oauth2.models import AccessToken, DeviceToken, RefreshToken
|
from authentik.providers.oauth2.models import AccessToken, DeviceToken, RefreshToken
|
||||||
|
|
||||||
|
|
||||||
@receiver(user_logged_out)
|
@receiver(user_logged_out)
|
||||||
def user_logged_out_oauth_tokens_removal(sender, request: HttpRequest, user: User, **_):
|
def user_logged_out_oauth_access_token(sender, request: HttpRequest, user: User, **_):
|
||||||
"""Revoke tokens upon user logout"""
|
"""Revoke access tokens upon user logout"""
|
||||||
if not request.session or not request.session.session_key:
|
if not request.session or not request.session.session_key:
|
||||||
return
|
return
|
||||||
AccessToken.objects.filter(
|
AccessToken.objects.filter(user=user, session__session_key=request.session.session_key).delete()
|
||||||
user=user,
|
|
||||||
session__session__session_key=request.session.session_key,
|
|
||||||
).delete()
|
|
||||||
|
|
||||||
|
|
||||||
@receiver(pre_delete, sender=AuthenticatedSession)
|
|
||||||
def user_session_deleted_oauth_tokens_removal(sender, instance: AuthenticatedSession, **_):
|
|
||||||
"""Revoke tokens upon user logout"""
|
|
||||||
AccessToken.objects.filter(
|
|
||||||
user=instance.user,
|
|
||||||
session__session__session_key=instance.session.session_key,
|
|
||||||
).delete()
|
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_save, sender=User)
|
@receiver(post_save, sender=User)
|
||||||
@ -32,6 +20,6 @@ def user_deactivated(sender, instance: User, **_):
|
|||||||
"""Remove user tokens when deactivated"""
|
"""Remove user tokens when deactivated"""
|
||||||
if instance.is_active:
|
if instance.is_active:
|
||||||
return
|
return
|
||||||
AccessToken.objects.filter(user=instance).delete()
|
AccessToken.objects.filter(session__user=instance).delete()
|
||||||
RefreshToken.objects.filter(user=instance).delete()
|
RefreshToken.objects.filter(session__user=instance).delete()
|
||||||
DeviceToken.objects.filter(user=instance).delete()
|
DeviceToken.objects.filter(session__user=instance).delete()
|
||||||
|
@ -7,13 +7,12 @@ from dataclasses import asdict
|
|||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
from authentik.core.models import Application, AuthenticatedSession, Session
|
from authentik.core.models import Application
|
||||||
from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow
|
from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow
|
||||||
from authentik.lib.generators import generate_id
|
from authentik.lib.generators import generate_id
|
||||||
from authentik.providers.oauth2.models import (
|
from authentik.providers.oauth2.models import (
|
||||||
AccessToken,
|
AccessToken,
|
||||||
ClientTypes,
|
ClientTypes,
|
||||||
DeviceToken,
|
|
||||||
IDToken,
|
IDToken,
|
||||||
OAuth2Provider,
|
OAuth2Provider,
|
||||||
RedirectURI,
|
RedirectURI,
|
||||||
@ -21,7 +20,6 @@ from authentik.providers.oauth2.models import (
|
|||||||
RefreshToken,
|
RefreshToken,
|
||||||
)
|
)
|
||||||
from authentik.providers.oauth2.tests.utils import OAuthTestCase
|
from authentik.providers.oauth2.tests.utils import OAuthTestCase
|
||||||
from authentik.root.middleware import ClientIPMiddleware
|
|
||||||
|
|
||||||
|
|
||||||
class TesOAuth2Revoke(OAuthTestCase):
|
class TesOAuth2Revoke(OAuthTestCase):
|
||||||
@ -137,86 +135,3 @@ class TesOAuth2Revoke(OAuthTestCase):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
self.assertEqual(res.status_code, 200)
|
self.assertEqual(res.status_code, 200)
|
||||||
|
|
||||||
def test_revoke_logout(self):
|
|
||||||
"""Test revoke on logout"""
|
|
||||||
self.client.force_login(self.user)
|
|
||||||
AccessToken.objects.create(
|
|
||||||
provider=self.provider,
|
|
||||||
user=self.user,
|
|
||||||
session=self.client.session["authenticatedsession"],
|
|
||||||
token=generate_id(),
|
|
||||||
auth_time=timezone.now(),
|
|
||||||
_scope="openid user profile",
|
|
||||||
_id_token=json.dumps(
|
|
||||||
asdict(
|
|
||||||
IDToken("foo", "bar"),
|
|
||||||
)
|
|
||||||
),
|
|
||||||
)
|
|
||||||
self.client.logout()
|
|
||||||
self.assertEqual(AccessToken.objects.all().count(), 0)
|
|
||||||
|
|
||||||
def test_revoke_session_delete(self):
|
|
||||||
"""Test revoke on logout"""
|
|
||||||
session = AuthenticatedSession.objects.create(
|
|
||||||
session=Session.objects.create(
|
|
||||||
session_key=generate_id(),
|
|
||||||
last_ip=ClientIPMiddleware.default_ip,
|
|
||||||
),
|
|
||||||
user=self.user,
|
|
||||||
)
|
|
||||||
AccessToken.objects.create(
|
|
||||||
provider=self.provider,
|
|
||||||
user=self.user,
|
|
||||||
session=session,
|
|
||||||
token=generate_id(),
|
|
||||||
auth_time=timezone.now(),
|
|
||||||
_scope="openid user profile",
|
|
||||||
_id_token=json.dumps(
|
|
||||||
asdict(
|
|
||||||
IDToken("foo", "bar"),
|
|
||||||
)
|
|
||||||
),
|
|
||||||
)
|
|
||||||
session.delete()
|
|
||||||
self.assertEqual(AccessToken.objects.all().count(), 0)
|
|
||||||
|
|
||||||
def test_revoke_user_deactivated(self):
|
|
||||||
"""Test revoke on logout"""
|
|
||||||
AccessToken.objects.create(
|
|
||||||
provider=self.provider,
|
|
||||||
user=self.user,
|
|
||||||
token=generate_id(),
|
|
||||||
auth_time=timezone.now(),
|
|
||||||
_scope="openid user profile",
|
|
||||||
_id_token=json.dumps(
|
|
||||||
asdict(
|
|
||||||
IDToken("foo", "bar"),
|
|
||||||
)
|
|
||||||
),
|
|
||||||
)
|
|
||||||
RefreshToken.objects.create(
|
|
||||||
provider=self.provider,
|
|
||||||
user=self.user,
|
|
||||||
token=generate_id(),
|
|
||||||
auth_time=timezone.now(),
|
|
||||||
_scope="openid user profile",
|
|
||||||
_id_token=json.dumps(
|
|
||||||
asdict(
|
|
||||||
IDToken("foo", "bar"),
|
|
||||||
)
|
|
||||||
),
|
|
||||||
)
|
|
||||||
DeviceToken.objects.create(
|
|
||||||
provider=self.provider,
|
|
||||||
user=self.user,
|
|
||||||
_scope="openid user profile",
|
|
||||||
)
|
|
||||||
|
|
||||||
self.user.is_active = False
|
|
||||||
self.user.save()
|
|
||||||
|
|
||||||
self.assertEqual(AccessToken.objects.all().count(), 0)
|
|
||||||
self.assertEqual(RefreshToken.objects.all().count(), 0)
|
|
||||||
self.assertEqual(DeviceToken.objects.all().count(), 0)
|
|
||||||
|
@ -15,7 +15,7 @@ from django.utils import timezone
|
|||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
from authentik.core.models import Application
|
from authentik.core.models import Application, AuthenticatedSession
|
||||||
from authentik.events.models import Event, EventAction
|
from authentik.events.models import Event, EventAction
|
||||||
from authentik.events.signals import get_login_event
|
from authentik.events.signals import get_login_event
|
||||||
from authentik.flows.challenge import (
|
from authentik.flows.challenge import (
|
||||||
@ -316,7 +316,9 @@ class OAuthAuthorizationParams:
|
|||||||
expires=now + timedelta_from_string(self.provider.access_code_validity),
|
expires=now + timedelta_from_string(self.provider.access_code_validity),
|
||||||
scope=self.scope,
|
scope=self.scope,
|
||||||
nonce=self.nonce,
|
nonce=self.nonce,
|
||||||
session=request.session["authenticatedsession"],
|
session=AuthenticatedSession.objects.filter(
|
||||||
|
session_key=request.session.session_key
|
||||||
|
).first(),
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.code_challenge and self.code_challenge_method:
|
if self.code_challenge and self.code_challenge_method:
|
||||||
@ -613,7 +615,9 @@ class OAuthFulfillmentStage(StageView):
|
|||||||
expires=access_token_expiry,
|
expires=access_token_expiry,
|
||||||
provider=self.provider,
|
provider=self.provider,
|
||||||
auth_time=auth_event.created if auth_event else now,
|
auth_time=auth_event.created if auth_event else now,
|
||||||
session=self.request.session["authenticatedsession"],
|
session=AuthenticatedSession.objects.filter(
|
||||||
|
session_key=self.request.session.session_key
|
||||||
|
).first(),
|
||||||
)
|
)
|
||||||
|
|
||||||
id_token = IDToken.new(self.provider, token, self.request)
|
id_token = IDToken.new(self.provider, token, self.request)
|
||||||
|
@ -20,4 +20,4 @@ def logout_proxy_revoke_direct(sender: type[User], request: HttpRequest, **_):
|
|||||||
@receiver(pre_delete, sender=AuthenticatedSession)
|
@receiver(pre_delete, sender=AuthenticatedSession)
|
||||||
def logout_proxy_revoke(sender: type[AuthenticatedSession], instance: AuthenticatedSession, **_):
|
def logout_proxy_revoke(sender: type[AuthenticatedSession], instance: AuthenticatedSession, **_):
|
||||||
"""Catch logout by expiring sessions being deleted"""
|
"""Catch logout by expiring sessions being deleted"""
|
||||||
proxy_on_logout.delay(instance.session.session_key)
|
proxy_on_logout.delay(instance.session_key)
|
||||||
|
@ -1,60 +0,0 @@
|
|||||||
# Generated by Django 5.0.11 on 2025-01-27 12:59
|
|
||||||
|
|
||||||
from django.db import migrations
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
def migrate_sessions(apps, schema_editor):
|
|
||||||
ConnectionToken = apps.get_model("authentik_providers_rac", "ConnectionToken")
|
|
||||||
AuthenticatedSession = apps.get_model("authentik_core", "AuthenticatedSession")
|
|
||||||
db_alias = schema_editor.connection.alias
|
|
||||||
|
|
||||||
for token in ConnectionToken.objects.using(db_alias).all():
|
|
||||||
token.session = (
|
|
||||||
AuthenticatedSession.objects.using(db_alias)
|
|
||||||
.filter(session_key=token.old_session.session_key)
|
|
||||||
.first()
|
|
||||||
)
|
|
||||||
if token.session:
|
|
||||||
token.save()
|
|
||||||
else:
|
|
||||||
token.delete()
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("authentik_providers_rac", "0006_connectiontoken_authentik_p_expires_91f148_idx_and_more"),
|
|
||||||
("authentik_core", "0046_session_and_more"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.RenameField(
|
|
||||||
model_name="connectiontoken",
|
|
||||||
old_name="session",
|
|
||||||
new_name="old_session",
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="connectiontoken",
|
|
||||||
name="session",
|
|
||||||
field=models.ForeignKey(
|
|
||||||
null=True,
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
to="authentik_core.authenticatedsession",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.RunPython(code=migrate_sessions),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="connectiontoken",
|
|
||||||
name="session",
|
|
||||||
field=models.ForeignKey(
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
to="authentik_core.authenticatedsession",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name="connectiontoken",
|
|
||||||
name="old_session",
|
|
||||||
),
|
|
||||||
]
|
|
@ -8,7 +8,7 @@ from django.db.models.signals import post_delete, post_save, pre_delete
|
|||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
|
|
||||||
from authentik.core.models import AuthenticatedSession, User
|
from authentik.core.models import User
|
||||||
from authentik.providers.rac.api.endpoints import user_endpoint_cache_key
|
from authentik.providers.rac.api.endpoints import user_endpoint_cache_key
|
||||||
from authentik.providers.rac.consumer_client import (
|
from authentik.providers.rac.consumer_client import (
|
||||||
RAC_CLIENT_GROUP_SESSION,
|
RAC_CLIENT_GROUP_SESSION,
|
||||||
@ -32,18 +32,6 @@ def user_logged_out_session(sender, request: HttpRequest, user: User, **_):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@receiver(pre_delete, sender=AuthenticatedSession)
|
|
||||||
def user_session_deleted(sender, instance: AuthenticatedSession, **_):
|
|
||||||
layer = get_channel_layer()
|
|
||||||
async_to_sync(layer.group_send)(
|
|
||||||
RAC_CLIENT_GROUP_SESSION
|
|
||||||
% {
|
|
||||||
"session": instance.session.session_key,
|
|
||||||
},
|
|
||||||
{"type": "event.disconnect", "reason": "session_logout"},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@receiver(pre_delete, sender=ConnectionToken)
|
@receiver(pre_delete, sender=ConnectionToken)
|
||||||
def pre_delete_connection_token_disconnect(sender, instance: ConnectionToken, **_):
|
def pre_delete_connection_token_disconnect(sender, instance: ConnectionToken, **_):
|
||||||
"""Disconnect session when connection token is deleted"""
|
"""Disconnect session when connection token is deleted"""
|
||||||
|
@ -4,13 +4,10 @@
|
|||||||
|
|
||||||
{% 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="#18191a" media="(prefers-color-scheme: dark)" />
|
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">
|
||||||
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)" />
|
<link rel="icon" href="{{ tenant.branding_favicon_url }}">
|
||||||
|
<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 %}
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from django.test import TransactionTestCase
|
from django.test import TransactionTestCase
|
||||||
|
|
||||||
from authentik.core.models import Application, AuthenticatedSession, Session
|
from authentik.core.models import Application, AuthenticatedSession
|
||||||
from authentik.core.tests.utils import create_test_admin_user
|
from authentik.core.tests.utils import create_test_admin_user
|
||||||
from authentik.lib.generators import generate_id
|
from authentik.lib.generators import generate_id
|
||||||
from authentik.providers.rac.models import (
|
from authentik.providers.rac.models import (
|
||||||
@ -36,15 +36,13 @@ class TestModels(TransactionTestCase):
|
|||||||
|
|
||||||
def test_settings_merge(self):
|
def test_settings_merge(self):
|
||||||
"""Test settings merge"""
|
"""Test settings merge"""
|
||||||
session = Session.objects.create(
|
|
||||||
session_key=generate_id(),
|
|
||||||
last_ip="255.255.255.255",
|
|
||||||
)
|
|
||||||
auth_session = AuthenticatedSession.objects.create(session=session, user=self.user)
|
|
||||||
token = ConnectionToken.objects.create(
|
token = ConnectionToken.objects.create(
|
||||||
provider=self.provider,
|
provider=self.provider,
|
||||||
endpoint=self.endpoint,
|
endpoint=self.endpoint,
|
||||||
session=auth_session,
|
session=AuthenticatedSession.objects.create(
|
||||||
|
user=self.user,
|
||||||
|
session_key=generate_id(),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
path = f"/tmp/connection/{token.token}" # nosec
|
path = f"/tmp/connection/{token.token}" # nosec
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
"""rac urls"""
|
"""rac urls"""
|
||||||
|
|
||||||
|
from channels.auth import AuthMiddleware
|
||||||
|
from channels.sessions import CookieMiddleware
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from authentik.outposts.channels import TokenOutpostMiddleware
|
from authentik.outposts.channels import TokenOutpostMiddleware
|
||||||
@ -10,7 +12,7 @@ from authentik.providers.rac.api.providers import RACProviderViewSet
|
|||||||
from authentik.providers.rac.consumer_client import RACClientConsumer
|
from authentik.providers.rac.consumer_client import RACClientConsumer
|
||||||
from authentik.providers.rac.consumer_outpost import RACOutpostConsumer
|
from authentik.providers.rac.consumer_outpost import RACOutpostConsumer
|
||||||
from authentik.providers.rac.views import RACInterface, RACStartView
|
from authentik.providers.rac.views import RACInterface, RACStartView
|
||||||
from authentik.root.asgi_middleware import AuthMiddlewareStack
|
from authentik.root.asgi_middleware import SessionMiddleware
|
||||||
from authentik.root.middleware import ChannelsLoggingMiddleware
|
from authentik.root.middleware import ChannelsLoggingMiddleware
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
@ -29,7 +31,9 @@ urlpatterns = [
|
|||||||
websocket_urlpatterns = [
|
websocket_urlpatterns = [
|
||||||
path(
|
path(
|
||||||
"ws/rac/<str:token>/",
|
"ws/rac/<str:token>/",
|
||||||
ChannelsLoggingMiddleware(AuthMiddlewareStack(RACClientConsumer.as_asgi())),
|
ChannelsLoggingMiddleware(
|
||||||
|
CookieMiddleware(SessionMiddleware(AuthMiddleware(RACClientConsumer.as_asgi())))
|
||||||
|
),
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"ws/outpost_rac/<str:channel>/",
|
"ws/outpost_rac/<str:channel>/",
|
||||||
|
@ -8,7 +8,7 @@ from django.urls import reverse
|
|||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
from authentik.core.models import Application
|
from authentik.core.models import Application, AuthenticatedSession
|
||||||
from authentik.core.views.interface import InterfaceView
|
from authentik.core.views.interface import InterfaceView
|
||||||
from authentik.events.models import Event, EventAction
|
from authentik.events.models import Event, EventAction
|
||||||
from authentik.flows.challenge import RedirectChallenge
|
from authentik.flows.challenge import RedirectChallenge
|
||||||
@ -113,7 +113,9 @@ class RACFinalStage(RedirectStage):
|
|||||||
provider=self.provider,
|
provider=self.provider,
|
||||||
endpoint=self.endpoint,
|
endpoint=self.endpoint,
|
||||||
settings=self.executor.plan.context.get("connection_settings", {}),
|
settings=self.executor.plan.context.get("connection_settings", {}),
|
||||||
session=self.request.session["authenticatedsession"],
|
session=AuthenticatedSession.objects.filter(
|
||||||
|
session_key=self.request.session.session_key
|
||||||
|
).first(),
|
||||||
expires=now() + timedelta_from_string(self.provider.connection_expiry),
|
expires=now() + timedelta_from_string(self.provider.connection_expiry),
|
||||||
expiring=True,
|
expiring=True,
|
||||||
)
|
)
|
||||||
|
@ -1,41 +0,0 @@
|
|||||||
"""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"]
|
|
@ -1,39 +0,0 @@
|
|||||||
# 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,7 +3,6 @@
|
|||||||
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 _
|
||||||
@ -76,35 +75,6 @@ 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,13 +1,9 @@
|
|||||||
"""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"""
|
||||||
@ -55,20 +51,3 @@ 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)
|
|
||||||
|
@ -1,116 +0,0 @@
|
|||||||
"""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,6 +1,5 @@
|
|||||||
"""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
|
||||||
@ -22,6 +21,5 @@ 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, "roles"),
|
("rbac/roles", RoleViewSet),
|
||||||
("rbac/initial_permissions", InitialPermissionsViewSet, "initial-permissions"),
|
|
||||||
]
|
]
|
||||||
|
@ -50,7 +50,7 @@ class TestRecovery(TestCase):
|
|||||||
)
|
)
|
||||||
token = Token.objects.get(intent=TokenIntents.INTENT_RECOVERY, user=self.user)
|
token = Token.objects.get(intent=TokenIntents.INTENT_RECOVERY, user=self.user)
|
||||||
self.client.get(reverse("authentik_recovery:use-token", kwargs={"key": token.key}))
|
self.client.get(reverse("authentik_recovery:use-token", kwargs={"key": token.key}))
|
||||||
self.assertEqual(self.client.session["authenticatedsession"].user.pk, token.user.pk)
|
self.assertEqual(int(self.client.session["_auth_user_id"]), token.user.pk)
|
||||||
|
|
||||||
def test_recovery_view_invalid(self):
|
def test_recovery_view_invalid(self):
|
||||||
"""Test recovery view with invalid token"""
|
"""Test recovery view with invalid token"""
|
||||||
|
@ -1,12 +1,8 @@
|
|||||||
"""ASGI middleware"""
|
"""ASGI middleware"""
|
||||||
|
|
||||||
from channels.auth import UserLazyObject
|
|
||||||
from channels.db import database_sync_to_async
|
from channels.db import database_sync_to_async
|
||||||
from channels.middleware import BaseMiddleware
|
|
||||||
from channels.sessions import CookieMiddleware
|
|
||||||
from channels.sessions import InstanceSessionWrapper as UpstreamInstanceSessionWrapper
|
from channels.sessions import InstanceSessionWrapper as UpstreamInstanceSessionWrapper
|
||||||
from channels.sessions import SessionMiddleware as UpstreamSessionMiddleware
|
from channels.sessions import SessionMiddleware as UpstreamSessionMiddleware
|
||||||
from django.contrib.auth.models import AnonymousUser
|
|
||||||
|
|
||||||
from authentik.root.middleware import SessionMiddleware as HTTPSessionMiddleware
|
from authentik.root.middleware import SessionMiddleware as HTTPSessionMiddleware
|
||||||
|
|
||||||
@ -37,48 +33,3 @@ class SessionMiddleware(UpstreamSessionMiddleware):
|
|||||||
await wrapper.resolve_session()
|
await wrapper.resolve_session()
|
||||||
|
|
||||||
return await self.inner(wrapper.scope, receive, wrapper.send)
|
return await self.inner(wrapper.scope, receive, wrapper.send)
|
||||||
|
|
||||||
|
|
||||||
@database_sync_to_async
|
|
||||||
def get_user(scope):
|
|
||||||
"""
|
|
||||||
Return the user model instance associated with the given scope.
|
|
||||||
If no user is retrieved, return an instance of `AnonymousUser`.
|
|
||||||
"""
|
|
||||||
if "session" not in scope:
|
|
||||||
raise ValueError(
|
|
||||||
"Cannot find session in scope. You should wrap your consumer in SessionMiddleware."
|
|
||||||
)
|
|
||||||
user = None
|
|
||||||
if (authenticated_session := scope["session"].get("authenticated_session", None)) is not None:
|
|
||||||
user = authenticated_session.user
|
|
||||||
return user or AnonymousUser()
|
|
||||||
|
|
||||||
|
|
||||||
class AuthMiddleware(BaseMiddleware):
|
|
||||||
def populate_scope(self, scope):
|
|
||||||
# Make sure we have a session
|
|
||||||
if "session" not in scope:
|
|
||||||
raise ValueError(
|
|
||||||
"AuthMiddleware cannot find session in scope. SessionMiddleware must be above it."
|
|
||||||
)
|
|
||||||
# Add it to the scope if it's not there already
|
|
||||||
if "user" not in scope:
|
|
||||||
scope["user"] = UserLazyObject()
|
|
||||||
|
|
||||||
async def resolve_scope(self, scope):
|
|
||||||
scope["user"]._wrapped = await get_user(scope)
|
|
||||||
|
|
||||||
async def __call__(self, scope, receive, send):
|
|
||||||
scope = dict(scope)
|
|
||||||
# Scope injection/mutation per this middleware's needs.
|
|
||||||
self.populate_scope(scope)
|
|
||||||
# Grab the finalized/resolved scope
|
|
||||||
await self.resolve_scope(scope)
|
|
||||||
|
|
||||||
return await super().__call__(scope, receive, send)
|
|
||||||
|
|
||||||
|
|
||||||
# Handy shortcut for applying all three layers at once
|
|
||||||
def AuthMiddlewareStack(inner):
|
|
||||||
return CookieMiddleware(SessionMiddleware(AuthMiddleware(inner)))
|
|
||||||
|
@ -49,7 +49,7 @@ class SessionMiddleware(UpstreamSessionMiddleware):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def decode_session_key(key: str | None) -> str | None:
|
def decode_session_key(key: str) -> str:
|
||||||
"""Decode raw session cookie, and parse JWT"""
|
"""Decode raw session cookie, and parse JWT"""
|
||||||
# We need to support the standard django format of just a session key
|
# We need to support the standard django format of just a session key
|
||||||
# for testing setups, where the session is directly set
|
# for testing setups, where the session is directly set
|
||||||
@ -64,11 +64,7 @@ class SessionMiddleware(UpstreamSessionMiddleware):
|
|||||||
def process_request(self, request: HttpRequest):
|
def process_request(self, request: HttpRequest):
|
||||||
raw_session = request.COOKIES.get(settings.SESSION_COOKIE_NAME)
|
raw_session = request.COOKIES.get(settings.SESSION_COOKIE_NAME)
|
||||||
session_key = SessionMiddleware.decode_session_key(raw_session)
|
session_key = SessionMiddleware.decode_session_key(raw_session)
|
||||||
request.session = self.SessionStore(
|
request.session = self.SessionStore(session_key)
|
||||||
session_key,
|
|
||||||
last_ip=ClientIPMiddleware.get_client_ip(request),
|
|
||||||
last_user_agent=request.META.get("HTTP_USER_AGENT", ""),
|
|
||||||
)
|
|
||||||
|
|
||||||
def process_response(self, request: HttpRequest, response: HttpResponse) -> HttpResponse:
|
def process_response(self, request: HttpRequest, response: HttpResponse) -> HttpResponse:
|
||||||
"""
|
"""
|
||||||
|
23
authentik/root/sessions/pickle.py
Normal file
23
authentik/root/sessions/pickle.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
"""
|
||||||
|
Module for abstract serializer/unserializer base classes.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pickle # nosec
|
||||||
|
|
||||||
|
|
||||||
|
class PickleSerializer:
|
||||||
|
"""
|
||||||
|
Simple wrapper around pickle to be used in signing.dumps()/loads() and
|
||||||
|
cache backends.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, protocol=None):
|
||||||
|
self.protocol = pickle.HIGHEST_PROTOCOL if protocol is None else protocol
|
||||||
|
|
||||||
|
def dumps(self, obj):
|
||||||
|
"""Pickle data to be stored in redis"""
|
||||||
|
return pickle.dumps(obj, self.protocol)
|
||||||
|
|
||||||
|
def loads(self, data):
|
||||||
|
"""Unpickle data to be loaded from redis"""
|
||||||
|
return pickle.loads(data) # nosec
|
@ -7,6 +7,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
import orjson
|
import orjson
|
||||||
from celery.schedules import crontab
|
from celery.schedules import crontab
|
||||||
|
from django.conf import ImproperlyConfigured
|
||||||
from sentry_sdk import set_tag
|
from sentry_sdk import set_tag
|
||||||
from xmlsec import enable_debug_trace
|
from xmlsec import enable_debug_trace
|
||||||
|
|
||||||
@ -42,6 +43,7 @@ SESSION_COOKIE_DOMAIN = CONFIG.get("cookie_domain", None)
|
|||||||
APPEND_SLASH = False
|
APPEND_SLASH = False
|
||||||
|
|
||||||
AUTHENTICATION_BACKENDS = [
|
AUTHENTICATION_BACKENDS = [
|
||||||
|
"django.contrib.auth.backends.ModelBackend",
|
||||||
BACKEND_INBUILT,
|
BACKEND_INBUILT,
|
||||||
BACKEND_APP_PASSWORD,
|
BACKEND_APP_PASSWORD,
|
||||||
BACKEND_LDAP,
|
BACKEND_LDAP,
|
||||||
@ -227,7 +229,17 @@ CACHES = {
|
|||||||
DJANGO_REDIS_SCAN_ITERSIZE = 1000
|
DJANGO_REDIS_SCAN_ITERSIZE = 1000
|
||||||
DJANGO_REDIS_IGNORE_EXCEPTIONS = True
|
DJANGO_REDIS_IGNORE_EXCEPTIONS = True
|
||||||
DJANGO_REDIS_LOG_IGNORED_EXCEPTIONS = True
|
DJANGO_REDIS_LOG_IGNORED_EXCEPTIONS = True
|
||||||
SESSION_ENGINE = "authentik.core.sessions"
|
match CONFIG.get("session_storage", "cache"):
|
||||||
|
case "cache":
|
||||||
|
SESSION_ENGINE = "django.contrib.sessions.backends.cache"
|
||||||
|
case "db":
|
||||||
|
SESSION_ENGINE = "django.contrib.sessions.backends.db"
|
||||||
|
case _:
|
||||||
|
raise ImproperlyConfigured(
|
||||||
|
"Invalid session_storage setting, allowed values are db and cache"
|
||||||
|
)
|
||||||
|
SESSION_SERIALIZER = "authentik.root.sessions.pickle.PickleSerializer"
|
||||||
|
SESSION_CACHE_ALIAS = "default"
|
||||||
# Configured via custom SessionMiddleware
|
# Configured via custom SessionMiddleware
|
||||||
# SESSION_COOKIE_SAMESITE = "None"
|
# SESSION_COOKIE_SAMESITE = "None"
|
||||||
# SESSION_COOKIE_SECURE = True
|
# SESSION_COOKIE_SECURE = True
|
||||||
@ -244,7 +256,7 @@ MIDDLEWARE = [
|
|||||||
"django_prometheus.middleware.PrometheusBeforeMiddleware",
|
"django_prometheus.middleware.PrometheusBeforeMiddleware",
|
||||||
"authentik.root.middleware.ClientIPMiddleware",
|
"authentik.root.middleware.ClientIPMiddleware",
|
||||||
"authentik.stages.user_login.middleware.BoundSessionMiddleware",
|
"authentik.stages.user_login.middleware.BoundSessionMiddleware",
|
||||||
"authentik.core.middleware.AuthenticationMiddleware",
|
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||||
"authentik.core.middleware.RequestIDMiddleware",
|
"authentik.core.middleware.RequestIDMiddleware",
|
||||||
"authentik.brands.middleware.BrandMiddleware",
|
"authentik.brands.middleware.BrandMiddleware",
|
||||||
"authentik.events.middleware.AuditMiddleware",
|
"authentik.events.middleware.AuditMiddleware",
|
||||||
|
@ -15,22 +15,11 @@ from rest_framework.response import Response
|
|||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
from authentik.core.api.property_mappings import PropertyMappingFilterSet, PropertyMappingSerializer
|
from authentik.core.api.property_mappings import PropertyMappingFilterSet, PropertyMappingSerializer
|
||||||
from authentik.core.api.sources import (
|
from authentik.core.api.sources import SourceSerializer
|
||||||
GroupSourceConnectionSerializer,
|
|
||||||
GroupSourceConnectionViewSet,
|
|
||||||
SourceSerializer,
|
|
||||||
UserSourceConnectionSerializer,
|
|
||||||
UserSourceConnectionViewSet,
|
|
||||||
)
|
|
||||||
from authentik.core.api.used_by import UsedByMixin
|
from authentik.core.api.used_by import UsedByMixin
|
||||||
from authentik.crypto.models import CertificateKeyPair
|
from authentik.crypto.models import CertificateKeyPair
|
||||||
from authentik.lib.sync.outgoing.api import SyncStatusSerializer
|
from authentik.lib.sync.outgoing.api import SyncStatusSerializer
|
||||||
from authentik.sources.ldap.models import (
|
from authentik.sources.ldap.models import LDAPSource, LDAPSourcePropertyMapping
|
||||||
GroupLDAPSourceConnection,
|
|
||||||
LDAPSource,
|
|
||||||
LDAPSourcePropertyMapping,
|
|
||||||
UserLDAPSourceConnection,
|
|
||||||
)
|
|
||||||
from authentik.sources.ldap.tasks import CACHE_KEY_STATUS, SYNC_CLASSES
|
from authentik.sources.ldap.tasks import CACHE_KEY_STATUS, SYNC_CLASSES
|
||||||
|
|
||||||
|
|
||||||
@ -232,23 +221,3 @@ class LDAPSourcePropertyMappingViewSet(UsedByMixin, ModelViewSet):
|
|||||||
filterset_class = LDAPSourcePropertyMappingFilter
|
filterset_class = LDAPSourcePropertyMappingFilter
|
||||||
search_fields = ["name"]
|
search_fields = ["name"]
|
||||||
ordering = ["name"]
|
ordering = ["name"]
|
||||||
|
|
||||||
|
|
||||||
class UserLDAPSourceConnectionSerializer(UserSourceConnectionSerializer):
|
|
||||||
class Meta(UserSourceConnectionSerializer.Meta):
|
|
||||||
model = UserLDAPSourceConnection
|
|
||||||
|
|
||||||
|
|
||||||
class UserLDAPSourceConnectionViewSet(UserSourceConnectionViewSet, ModelViewSet):
|
|
||||||
queryset = UserLDAPSourceConnection.objects.all()
|
|
||||||
serializer_class = UserLDAPSourceConnectionSerializer
|
|
||||||
|
|
||||||
|
|
||||||
class GroupLDAPSourceConnectionSerializer(GroupSourceConnectionSerializer):
|
|
||||||
class Meta(GroupSourceConnectionSerializer.Meta):
|
|
||||||
model = GroupLDAPSourceConnection
|
|
||||||
|
|
||||||
|
|
||||||
class GroupLDAPSourceConnectionViewSet(GroupSourceConnectionViewSet, ModelViewSet):
|
|
||||||
queryset = GroupLDAPSourceConnection.objects.all()
|
|
||||||
serializer_class = GroupLDAPSourceConnectionSerializer
|
|
||||||
|
@ -1,57 +0,0 @@
|
|||||||
# Generated by Django 5.0.14 on 2025-04-11 11:46
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("authentik_core", "0047_delete_oldauthenticatedsession"),
|
|
||||||
("authentik_sources_ldap", "0007_ldapsource_lookup_groups_from_user"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name="GroupLDAPSourceConnection",
|
|
||||||
fields=[
|
|
||||||
(
|
|
||||||
"groupsourceconnection_ptr",
|
|
||||||
models.OneToOneField(
|
|
||||||
auto_created=True,
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
parent_link=True,
|
|
||||||
primary_key=True,
|
|
||||||
serialize=False,
|
|
||||||
to="authentik_core.groupsourceconnection",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
"verbose_name": "Group LDAP Source Connection",
|
|
||||||
"verbose_name_plural": "Group LDAP Source Connections",
|
|
||||||
},
|
|
||||||
bases=("authentik_core.groupsourceconnection",),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name="UserLDAPSourceConnection",
|
|
||||||
fields=[
|
|
||||||
(
|
|
||||||
"usersourceconnection_ptr",
|
|
||||||
models.OneToOneField(
|
|
||||||
auto_created=True,
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
parent_link=True,
|
|
||||||
primary_key=True,
|
|
||||||
serialize=False,
|
|
||||||
to="authentik_core.usersourceconnection",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
"verbose_name": "User LDAP Source Connection",
|
|
||||||
"verbose_name_plural": "User LDAP Source Connections",
|
|
||||||
},
|
|
||||||
bases=("authentik_core.usersourceconnection",),
|
|
||||||
),
|
|
||||||
]
|
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user