Compare commits

..

6 Commits

Author SHA1 Message Date
f059b998cc release: 2023.3.1 2023-03-16 18:09:53 +01:00
3f48202dfe web/flows: fix authenticator selector in dark mode (#4974)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-03-16 16:57:15 +01:00
2a3ebb616b providers/oauth2: fix response for response_type code and response_mode fragment (#4975) 2023-03-16 16:57:09 +01:00
ceab1f732d providers/ldap: fix duplicate attributes (#4972)
closes #4971

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-03-16 12:14:57 +01:00
01d2cce9ca Merge branch 'main' into version-2023.3 2023-03-15 20:20:51 +01:00
72f85defb8 release: 2023.3.0 2023-03-13 18:30:48 +01:00
385 changed files with 19746 additions and 25583 deletions

View File

@ -1,5 +1,5 @@
[bumpversion] [bumpversion]
current_version = 2023.4.2 current_version = 2023.3.1
tag = True tag = True
commit = True commit = True
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+) parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)

View File

@ -15,6 +15,3 @@ indent_size = 2
[*.go] [*.go]
indent_style = tab indent_style = tab
[Makefile]
indent_style = tab

View File

@ -1,11 +1,6 @@
name: 'Setup authentik testing environment' name: 'Setup authentik testing environment'
description: 'Setup authentik testing environment' description: 'Setup authentik testing environment'
inputs:
postgresql_tag:
description: "Optional postgresql image tag"
default: "12"
runs: runs:
using: "composite" using: "composite"
steps: steps:
@ -29,7 +24,6 @@ runs:
- name: Setup dependencies - name: Setup dependencies
shell: bash shell: bash
run: | run: |
export PSQL_TAG=${{ inputs.postgresql_tag }}
docker-compose -f .github/actions/setup/docker-compose.yml up -d docker-compose -f .github/actions/setup/docker-compose.yml up -d
poetry env use python3.11 poetry env use python3.11
poetry install poetry install

View File

@ -3,7 +3,7 @@ version: '3.7'
services: services:
postgresql: postgresql:
container_name: postgres container_name: postgres
image: library/postgres:${PSQL_TAG:-12} image: library/postgres:12
volumes: volumes:
- db-data:/var/lib/postgresql/data - db-data:/var/lib/postgresql/data
environment: environment:

11
.github/codecov.yml vendored
View File

@ -1,10 +1,3 @@
coverage: coverage:
status: precision: 2
project: round: up
default:
target: auto
# adjust accordingly based on how flaky your tests are
# this allows a 1% drop from the previous base commit coverage
threshold: 1%
notify:
after_n_builds: 3

View File

@ -1 +0,0 @@
authentic->authentik

1
.github/stale.yml vendored
View File

@ -16,4 +16,3 @@ markComment: >
This issue has been automatically marked as stale because it has not had This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Thank you recent activity. It will be closed if no further activity occurs. Thank you
for your contributions. for your contributions.
only: issues

View File

@ -29,7 +29,6 @@ jobs:
- bandit - bandit
- pyright - pyright
- pending-migrations - pending-migrations
- codespell
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
@ -60,7 +59,7 @@ jobs:
cp authentik/lib/default.yml local.env.yml cp authentik/lib/default.yml local.env.yml
cp -R .github .. cp -R .github ..
cp -R scripts .. cp -R scripts ..
git checkout $(git describe --tags $(git rev-list --tags --max-count=1)) git checkout $(git describe --abbrev=0 --match 'version/*')
rm -rf .github/ scripts/ rm -rf .github/ scripts/
mv ../.github ../scripts . mv ../.github ../scripts .
- name: Setup authentik env (ensure stable deps are installed) - name: Setup authentik env (ensure stable deps are installed)
@ -80,21 +79,12 @@ jobs:
- name: migrate to latest - name: migrate to latest
run: poetry run python -m lifecycle.migrate run: poetry run python -m lifecycle.migrate
test-unittest: test-unittest:
name: test-unittest - PostgreSQL ${{ matrix.psql }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 30 timeout-minutes: 30
strategy:
fail-fast: false
matrix:
psql:
- 11-alpine
- 12-alpine
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: Setup authentik env - name: Setup authentik env
uses: ./.github/actions/setup uses: ./.github/actions/setup
with:
postgresql_tag: ${{ matrix.psql }}
- name: run unittest - name: run unittest
run: | run: |
poetry run make test poetry run make test
@ -138,8 +128,6 @@ jobs:
glob: tests/e2e/test_provider_saml* tests/e2e/test_source_saml* glob: tests/e2e/test_provider_saml* tests/e2e/test_source_saml*
- name: ldap - name: ldap
glob: tests/e2e/test_provider_ldap* tests/e2e/test_source_ldap* glob: tests/e2e/test_provider_ldap* tests/e2e/test_source_ldap*
- name: radius
glob: tests/e2e/test_provider_radius*
- name: flows - name: flows
glob: tests/e2e/test_flows* glob: tests/e2e/test_flows*
steps: steps:

View File

@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-go@v4 - uses: actions/setup-go@v3
with: with:
go-version: "^1.17" go-version: "^1.17"
- name: Prepare and generate API - name: Prepare and generate API
@ -34,7 +34,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-go@v4 - uses: actions/setup-go@v3
with: with:
go-version: "^1.17" go-version: "^1.17"
- name: Generate API - name: Generate API
@ -59,9 +59,8 @@ jobs:
type: type:
- proxy - proxy
- ldap - ldap
- radius
arch: arch:
- "linux/amd64" - 'linux/amd64'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
@ -107,18 +106,17 @@ jobs:
type: type:
- proxy - proxy
- ldap - ldap
- radius
goos: [linux] goos: [linux]
goarch: [amd64, arm64] goarch: [amd64, arm64]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-go@v4 - uses: actions/setup-go@v3
with: with:
go-version: "^1.17" go-version: "^1.17"
- uses: actions/setup-node@v3.6.0 - uses: actions/setup-node@v3.6.0
with: with:
node-version: "18" node-version: '18'
cache: "npm" cache: 'npm'
cache-dependency-path: web/package-lock.json cache-dependency-path: web/package-lock.json
- name: Generate API - name: Generate API
run: make gen-client-go run: make gen-client-go

View File

@ -39,32 +39,10 @@ jobs:
- name: test - name: test
working-directory: website/ working-directory: website/
run: npm test run: npm test
build:
runs-on: ubuntu-latest
name: ${{ matrix.job }}
strategy:
fail-fast: false
matrix:
job:
- build
- build-docs-only
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3.6.0
with:
node-version: '18'
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:
needs: needs:
- lint-prettier - lint-prettier
- test - test
- build
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- run: echo mark - run: echo mark

View File

@ -10,11 +10,6 @@ jobs:
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
uses: tibdex/github-app-token@v1
with:
app_id: ${{ secrets.GH_APP_ID }}
private_key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- name: Delete 'dev' containers older than a week - name: Delete 'dev' containers older than a week
uses: snok/container-retention-policy@v2 uses: snok/container-retention-policy@v2
with: with:
@ -23,5 +18,5 @@ jobs:
account-type: org account-type: org
org-name: goauthentik org-name: goauthentik
untagged-only: false untagged-only: false
token: ${{ steps.generate_token.outputs.token }} token: ${{ secrets.BOT_GITHUB_TOKEN }}
skip-tags: gh-next,gh-main skip-tags: gh-next,gh-main

View File

@ -52,10 +52,9 @@ jobs:
type: type:
- proxy - proxy
- ldap - ldap
- radius
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-go@v4 - uses: actions/setup-go@v3
with: with:
go-version: "^1.17" go-version: "^1.17"
- name: Set up QEMU - name: Set up QEMU
@ -100,12 +99,11 @@ jobs:
type: type:
- proxy - proxy
- ldap - ldap
- radius
goos: [linux, darwin] goos: [linux, darwin]
goarch: [amd64, arm64] goarch: [amd64, arm64]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-go@v4 - uses: actions/setup-go@v3
with: with:
go-version: "^1.17" go-version: "^1.17"
- uses: actions/setup-node@v3.6.0 - uses: actions/setup-node@v3.6.0

View File

@ -22,23 +22,18 @@ jobs:
docker-compose up --no-start docker-compose up --no-start
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
- id: generate_token
uses: tibdex/github-app-token@v1
with:
app_id: ${{ secrets.GH_APP_ID }}
private_key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- name: Extract version number - name: Extract version number
id: get_version id: get_version
uses: actions/github-script@v6 uses: actions/github-script@v6
with: with:
github-token: ${{ steps.generate_token.outputs.token }} github-token: ${{ secrets.BOT_GITHUB_TOKEN }}
script: | script: |
return context.payload.ref.replace(/\/refs\/tags\/version\//, ''); return context.payload.ref.replace(/\/refs\/tags\/version\//, '');
- name: Create Release - name: Create Release
id: create_release id: create_release
uses: actions/create-release@v1.1.4 uses: actions/create-release@v1.1.4
env: env:
GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }} GITHUB_TOKEN: ${{ secrets.BOT_GITHUB_TOKEN }}
with: with:
tag_name: ${{ github.ref }} tag_name: ${{ github.ref }}
release_name: Release ${{ steps.get_version.outputs.result }} release_name: Release ${{ steps.get_version.outputs.result }}

View File

@ -18,23 +18,18 @@ jobs:
compile: compile:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- id: generate_token
uses: tibdex/github-app-token@v1
with:
app_id: ${{ secrets.GH_APP_ID }}
private_key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- uses: actions/checkout@v3 - uses: actions/checkout@v3
with: with:
token: ${{ steps.generate_token.outputs.token }} token: ${{ secrets.BOT_GITHUB_TOKEN }}
- name: Setup authentik env - name: Setup authentik env
uses: ./.github/actions/setup uses: ./.github/actions/setup
- name: run compile - name: run compile
run: poetry run ./manage.py compilemessages run: poetry run ./manage.py compilemessages
- name: Create Pull Request - name: Create Pull Request
uses: peter-evans/create-pull-request@v5 uses: peter-evans/create-pull-request@v4
id: cpr id: cpr
with: with:
token: ${{ steps.generate_token.outputs.token }} token: ${{ secrets.BOT_GITHUB_TOKEN }}
branch: compile-backend-translation branch: compile-backend-translation
commit-message: "core: compile backend translations" commit-message: "core: compile backend translations"
title: "core: compile backend translations" title: "core: compile backend translations"

View File

@ -9,14 +9,9 @@ jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- id: generate_token
uses: tibdex/github-app-token@v1
with:
app_id: ${{ secrets.GH_APP_ID }}
private_key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- uses: actions/checkout@v3 - uses: actions/checkout@v3
with: with:
token: ${{ steps.generate_token.outputs.token }} token: ${{ secrets.BOT_GITHUB_TOKEN }}
- uses: actions/setup-node@v3.6.0 - uses: actions/setup-node@v3.6.0
with: with:
node-version: '18' node-version: '18'
@ -35,10 +30,10 @@ jobs:
run: | run: |
export VERSION=`node -e 'console.log(require("../gen-ts-api/package.json").version)'` export VERSION=`node -e 'console.log(require("../gen-ts-api/package.json").version)'`
npm i @goauthentik/api@$VERSION npm i @goauthentik/api@$VERSION
- uses: peter-evans/create-pull-request@v5 - uses: peter-evans/create-pull-request@v4
id: cpr id: cpr
with: with:
token: ${{ steps.generate_token.outputs.token }} token: ${{ secrets.BOT_GITHUB_TOKEN }}
branch: update-web-api-client branch: update-web-api-client
commit-message: "web: bump API Client version" commit-message: "web: bump API Client version"
title: "web: bump API Client version" title: "web: bump API Client version"
@ -47,8 +42,8 @@ jobs:
signoff: true signoff: true
team-reviewers: "@goauthentik/core" team-reviewers: "@goauthentik/core"
author: authentik bot <github-bot@goauthentik.io> author: authentik bot <github-bot@goauthentik.io>
- uses: peter-evans/enable-pull-request-automerge@v3 - uses: peter-evans/enable-pull-request-automerge@v2
with: with:
token: ${{ steps.generate_token.outputs.token }} token: ${{ secrets.BOT_GITHUB_TOKEN }}
pull-request-number: ${{ steps.cpr.outputs.pull-request-number }} pull-request-number: ${{ steps.cpr.outputs.pull-request-number }}
merge-method: squash merge-method: squash

View File

@ -20,7 +20,6 @@ The following is a set of guidelines for contributing to authentik and its compo
- [Reporting Bugs](#reporting-bugs) - [Reporting Bugs](#reporting-bugs)
- [Suggesting Enhancements](#suggesting-enhancements) - [Suggesting Enhancements](#suggesting-enhancements)
- [Your First Code Contribution](#your-first-code-contribution) - [Your First Code Contribution](#your-first-code-contribution)
- [Help with the Docs](#help-with-the-docs)
- [Pull Requests](#pull-requests) - [Pull Requests](#pull-requests)
[Styleguides](#styleguides) [Styleguides](#styleguides)
@ -136,9 +135,6 @@ authentik can be run locally, all though depending on which part you want to wor
This is documented in the [developer docs](https://goauthentik.io/developer-docs/?utm_source=github) This is documented in the [developer docs](https://goauthentik.io/developer-docs/?utm_source=github)
### Help with the Docs
Contributions to the technical documentation are greatly appreciated. Open a PR if you have improvements to make or new content to add. If you have questions or suggestions about the documentation, open an Issue. No contribution is too small.
### Pull Requests ### Pull Requests
The process described here has several goals: The process described here has several goals:

View File

@ -20,7 +20,7 @@ WORKDIR /work/web
RUN npm ci && npm run build RUN npm ci && npm run build
# Stage 3: Poetry to requirements.txt export # Stage 3: Poetry to requirements.txt export
FROM docker.io/python:3.11.3-slim-bullseye AS poetry-locker FROM docker.io/python:3.11.2-slim-bullseye AS poetry-locker
WORKDIR /work WORKDIR /work
COPY ./pyproject.toml /work COPY ./pyproject.toml /work
@ -31,7 +31,7 @@ RUN pip install --no-cache-dir poetry && \
poetry export -f requirements.txt --dev --output requirements-dev.txt poetry export -f requirements.txt --dev --output requirements-dev.txt
# Stage 4: Build go proxy # Stage 4: Build go proxy
FROM docker.io/golang:1.20.3-bullseye AS go-builder FROM docker.io/golang:1.20.2-bullseye AS go-builder
WORKDIR /work WORKDIR /work
@ -47,7 +47,7 @@ COPY ./go.sum /work/go.sum
RUN go build -o /work/authentik ./cmd/server/ RUN go build -o /work/authentik ./cmd/server/
# Stage 5: MaxMind GeoIP # Stage 5: MaxMind GeoIP
FROM docker.io/maxmindinc/geoipupdate:v5.0 as geoip FROM docker.io/maxmindinc/geoipupdate:v4.10 as geoip
ENV GEOIPUPDATE_EDITION_IDS="GeoLite2-City" ENV GEOIPUPDATE_EDITION_IDS="GeoLite2-City"
ENV GEOIPUPDATE_VERBOSE="true" ENV GEOIPUPDATE_VERBOSE="true"
@ -62,7 +62,7 @@ RUN --mount=type=secret,id=GEOIPUPDATE_ACCOUNT_ID \
" "
# Stage 6: Run # Stage 6: Run
FROM docker.io/python:3.11.3-slim-bullseye AS final-image FROM docker.io/python:3.11.2-slim-bullseye AS final-image
LABEL org.opencontainers.image.url https://goauthentik.io LABEL org.opencontainers.image.url https://goauthentik.io
LABEL org.opencontainers.image.description goauthentik.io Main server image, see https://goauthentik.io for more info. LABEL org.opencontainers.image.description goauthentik.io Main server image, see https://goauthentik.io for more info.
@ -102,7 +102,7 @@ COPY ./tests /tests
COPY ./manage.py / COPY ./manage.py /
COPY ./blueprints /blueprints COPY ./blueprints /blueprints
COPY ./lifecycle/ /lifecycle COPY ./lifecycle/ /lifecycle
COPY --from=go-builder /work/authentik /bin/authentik COPY --from=go-builder /work/authentik /authentik-proxy
COPY --from=web-builder /work/web/dist/ /web/dist/ COPY --from=web-builder /work/web/dist/ /web/dist/
COPY --from=web-builder /work/web/authentik/ /web/authentik/ COPY --from=web-builder /work/web/authentik/ /web/authentik/
COPY --from=website-builder /work/website/help/ /website/help/ COPY --from=website-builder /work/website/help/ /website/help/

View File

@ -4,20 +4,6 @@ UID = $(shell id -u)
GID = $(shell id -g) GID = $(shell id -g)
NPM_VERSION = $(shell python -m scripts.npm_version) NPM_VERSION = $(shell python -m scripts.npm_version)
CODESPELL_ARGS = -D - -D .github/codespell-dictionary.txt \
-I .github/codespell-words.txt \
-S 'web/src/locales/**' \
authentik \
internal \
cmd \
web/src \
website/src \
website/blog \
website/developer-docs \
website/docs \
website/integrations \
website/src
all: lint-fix lint test gen web all: lint-fix lint test gen web
test-go: test-go:
@ -40,7 +26,14 @@ test:
lint-fix: lint-fix:
isort authentik tests scripts lifecycle isort authentik tests scripts lifecycle
black authentik tests scripts lifecycle black authentik tests scripts lifecycle
codespell -w $(CODESPELL_ARGS) codespell -I .github/codespell-words.txt -S 'web/src/locales/**' -w \
authentik \
internal \
cmd \
web/src \
website/src \
website/docs \
website/developer-docs
lint: lint:
pylint authentik tests lifecycle pylint authentik tests lifecycle
@ -50,6 +43,9 @@ lint:
migrate: migrate:
python -m lifecycle.migrate python -m lifecycle.migrate
run:
go run -v ./cmd/server/
i18n-extract: i18n-extract-core web-extract i18n-extract: i18n-extract-core web-extract
i18n-extract-core: i18n-extract-core:
@ -63,20 +59,15 @@ gen-build:
AUTHENTIK_DEBUG=true ak make_blueprint_schema > blueprints/schema.json AUTHENTIK_DEBUG=true ak make_blueprint_schema > blueprints/schema.json
AUTHENTIK_DEBUG=true ak spectacular --file schema.yml AUTHENTIK_DEBUG=true ak spectacular --file schema.yml
gen-changelog:
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
gen-diff: gen-diff:
git show $(shell git describe --tags $(shell git rev-list --tags --max-count=1)):schema.yml > old_schema.yml git show $(shell git describe --abbrev=0):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.6 \ docker.io/openapitools/openapi-diff:2.1.0-beta.3 \
--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
npx prettier --write diff.md
gen-clean: gen-clean:
rm -rf web/api/src/ rm -rf web/api/src/
@ -86,7 +77,7 @@ gen-client-ts:
docker run \ docker run \
--rm -v ${PWD}:/local \ --rm -v ${PWD}:/local \
--user ${UID}:${GID} \ --user ${UID}:${GID} \
docker.io/openapitools/openapi-generator-cli:v6.5.0 generate \ docker.io/openapitools/openapi-generator-cli:v6.0.0 generate \
-i /local/schema.yml \ -i /local/schema.yml \
-g typescript-fetch \ -g typescript-fetch \
-o /local/gen-ts-api \ -o /local/gen-ts-api \
@ -99,21 +90,20 @@ gen-client-ts:
\cp -rfv gen-ts-api/* web/node_modules/@goauthentik/api \cp -rfv gen-ts-api/* web/node_modules/@goauthentik/api
gen-client-go: gen-client-go:
mkdir -p ./gen-go-api ./gen-go-api/templates wget https://raw.githubusercontent.com/goauthentik/client-go/main/config.yaml -O config.yaml
wget https://raw.githubusercontent.com/goauthentik/client-go/main/config.yaml -O ./gen-go-api/config.yaml mkdir -p templates
wget https://raw.githubusercontent.com/goauthentik/client-go/main/templates/README.mustache -O ./gen-go-api/templates/README.mustache wget https://raw.githubusercontent.com/goauthentik/client-go/main/templates/README.mustache -O templates/README.mustache
wget https://raw.githubusercontent.com/goauthentik/client-go/main/templates/go.mod.mustache -O ./gen-go-api/templates/go.mod.mustache wget https://raw.githubusercontent.com/goauthentik/client-go/main/templates/go.mod.mustache -O templates/go.mod.mustache
cp schema.yml ./gen-go-api/
docker run \ docker run \
--rm -v ${PWD}/gen-go-api:/local \ --rm -v ${PWD}:/local \
--user ${UID}:${GID} \ --user ${UID}:${GID} \
docker.io/openapitools/openapi-generator-cli:v6.5.0 generate \ docker.io/openapitools/openapi-generator-cli:v6.0.0 generate \
-i /local/schema.yml \ -i /local/schema.yml \
-g go \ -g go \
-o /local/ \ -o /local/gen-go-api \
-c /local/config.yaml -c /local/config.yaml
go mod edit -replace goauthentik.io/api/v3=./gen-go-api go mod edit -replace goauthentik.io/api/v3=./gen-go-api
rm -rf ./gen-go-api/config.yaml ./gen-go-api/templates/ rm -rf config.yaml ./templates/
gen-dev-config: gen-dev-config:
python -m scripts.generate_config python -m scripts.generate_config
@ -182,9 +172,6 @@ ci-pylint: ci--meta-debug
ci-black: ci--meta-debug ci-black: ci--meta-debug
black --check $(PY_SOURCES) black --check $(PY_SOURCES)
ci-codespell: ci--meta-debug
codespell $(CODESPELL_ARGS) -s
ci-isort: ci--meta-debug ci-isort: ci--meta-debug
isort --check $(PY_SOURCES) isort --check $(PY_SOURCES)

View File

@ -15,13 +15,13 @@
## What is authentik? ## What is authentik?
Authentik is an open-source Identity Provider that emphasizes flexibility and versatility. It can be seamlessly integrated into existing environments to support new protocols. Authentik is also a great solution for implementing sign-up, recovery, and other similar features in your application, saving you the hassle of dealing with them. authentik is an open-source Identity Provider focused on flexibility and versatility. You can use authentik in an existing environment to add support for new protocols. authentik is also a great solution for implementing signup/recovery/etc in your application, so you don't have to deal with it.
## Installation ## Installation
For small/test setups it is recommended to use Docker Compose; refer to the [documentation](https://goauthentik.io/docs/installation/docker-compose/?utm_source=github). For small/test setups it is recommended to use docker-compose, see the [documentation](https://goauthentik.io/docs/installation/docker-compose/?utm_source=github)
For bigger setups, there is a Helm Chart [here](https://github.com/goauthentik/helm). This is documented [here](https://goauthentik.io/docs/installation/kubernetes/?utm_source=github). For bigger setups, there is a Helm Chart [here](https://github.com/goauthentik/helm). This is documented [here](https://goauthentik.io/docs/installation/kubernetes/?utm_source=github)
## Screenshots ## Screenshots
@ -32,15 +32,15 @@ For bigger setups, there is a Helm Chart [here](https://github.com/goauthentik/h
## Development ## Development
See [Developer Documentation](https://goauthentik.io/developer-docs/?utm_source=github) See [Development Documentation](https://goauthentik.io/developer-docs/?utm_source=github)
## Security ## Security
See [SECURITY.md](SECURITY.md) See [SECURITY.md](SECURITY.md)
## Adoption and Contributions ## Support
Your organization uses authentik? We'd love to add your logo to the readme and our website! Email us @ hello@goauthentik.io or open a GitHub Issue/PR! For more information on how to contribute to authentik, please refer to our [CONTRIBUTING.md file](./CONTRIBUTING.md). Your organization uses authentik? We'd love to add your logo to the readme and our website! Email us @ hello@goauthentik.io or open a GitHub Issue/PR!
## Sponsors ## Sponsors

View File

@ -2,7 +2,7 @@
from os import environ from os import environ
from typing import Optional from typing import Optional
__version__ = "2023.4.2" __version__ = "2023.3.1"
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH" ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"

View File

@ -1,4 +1,5 @@
"""authentik administration overview""" """authentik administration overview"""
import os
import platform import platform
from datetime import datetime from datetime import datetime
from sys import version as python_version from sys import version as python_version
@ -33,6 +34,7 @@ class RuntimeDict(TypedDict):
class SystemSerializer(PassiveSerializer): class SystemSerializer(PassiveSerializer):
"""Get system information.""" """Get system information."""
env = SerializerMethodField()
http_headers = SerializerMethodField() http_headers = SerializerMethodField()
http_host = SerializerMethodField() http_host = SerializerMethodField()
http_is_secure = SerializerMethodField() http_is_secure = SerializerMethodField()
@ -41,6 +43,10 @@ class SystemSerializer(PassiveSerializer):
server_time = SerializerMethodField() server_time = SerializerMethodField()
embedded_outpost_host = SerializerMethodField() embedded_outpost_host = SerializerMethodField()
def get_env(self, request: Request) -> dict[str, str]:
"""Get Environment"""
return os.environ.copy()
def get_http_headers(self, request: Request) -> dict[str, str]: def get_http_headers(self, request: Request) -> dict[str, str]:
"""Get HTTP Request headers""" """Get HTTP Request headers"""
headers = {} headers = {}

View File

@ -1,5 +1,4 @@
"""API Authentication""" """API Authentication"""
from hmac import compare_digest
from typing import Any, Optional from typing import Any, Optional
from django.conf import settings from django.conf import settings
@ -79,7 +78,7 @@ def token_secret_key(value: str) -> Optional[User]:
and return the service account for the managed outpost""" and return the service account for the managed outpost"""
from authentik.outposts.apps import MANAGED_OUTPOST from authentik.outposts.apps import MANAGED_OUTPOST
if not compare_digest(value, settings.SECRET_KEY): if value != settings.SECRET_KEY:
return None return None
outposts = Outpost.objects.filter(managed=MANAGED_OUTPOST) outposts = Outpost.objects.filter(managed=MANAGED_OUTPOST)
if not outposts: if not outposts:

View File

@ -7,13 +7,82 @@ API Browser - {{ tenant.branding_title }}
{% endblock %} {% endblock %}
{% block head %} {% block head %}
<script src="{% static 'dist/standalone/api-browser/index.js' %}?version={{ version }}" type="module"></script> <script type="module" src="{% static 'dist/rapidoc-min.js' %}"></script>
<meta name="theme-color" content="#151515" media="(prefers-color-scheme: light)"> <script>
<meta name="theme-color" content="#151515" media="(prefers-color-scheme: dark)"> function getCookie(name) {
<link rel="icon" href="{{ tenant.branding_favicon }}"> let cookieValue = "";
<link rel="shortcut icon" href="{{ tenant.branding_favicon }}"> if (document.cookie && document.cookie !== "") {
const cookies = document.cookie.split(";");
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
// Does this cookie string begin with the name we want?
if (cookie.substring(0, name.length + 1) === name + "=") {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
window.addEventListener('DOMContentLoaded', (event) => {
const rapidocEl = document.querySelector('rapi-doc');
rapidocEl.addEventListener('before-try', (e) => {
e.detail.request.headers.append('X-authentik-CSRF', getCookie("authentik_csrf"));
});
});
</script>
<style>
img.logo {
width: 100%;
padding: 1rem 0.5rem 1.5rem 0.5rem;
min-height: 48px;
}
</style>
{% endblock %} {% endblock %}
{% block body %} {% block body %}
<ak-api-browser schemaPath="{{ path }}"></ak-api-browser> <rapi-doc
spec-url="{{ path }}"
heading-text=""
theme="light"
render-style="read"
default-schema-tab="schema"
primary-color="#fd4b2d"
nav-bg-color="#212427"
bg-color="#000000"
text-color="#000000"
nav-text-color="#ffffff"
nav-hover-bg-color="#3c3f42"
nav-accent-color="#4f5255"
nav-hover-text-color="#ffffff"
use-path-in-nav-bar="true"
nav-item-spacing="relaxed"
allow-server-selection="false"
show-header="false"
allow-spec-url-load="false"
allow-spec-file-load="false">
<div slot="nav-logo">
<img alt="authentik Logo" class="logo" src="{% static 'dist/assets/icons/icon_left_brand.png' %}" />
</div>
</rapi-doc>
<script>
const rapidoc = document.querySelector("rapi-doc");
const matcher = window.matchMedia("(prefers-color-scheme: light)");
const changer = (ev) => {
const style = getComputedStyle(document.documentElement);
let bg, text = "";
if (matcher.matches) {
bg = style.getPropertyValue('--pf-global--BackgroundColor--light-300');
text = style.getPropertyValue('--pf-global--Color--300');
} else {
bg = style.getPropertyValue('--ak-dark-background');
text = style.getPropertyValue('--ak-dark-foreground');
}
rapidoc.attributes.getNamedItem("bg-color").value = bg.trim();
rapidoc.attributes.getNamedItem("text-color").value = text.trim();
rapidoc.requestUpdate();
};
matcher.addEventListener("change", changer);
window.addEventListener("load", changer);
</script>
{% endblock %} {% endblock %}

View File

@ -56,7 +56,6 @@ from authentik.providers.oauth2.api.tokens import (
RefreshTokenViewSet, RefreshTokenViewSet,
) )
from authentik.providers.proxy.api import ProxyOutpostConfigViewSet, ProxyProviderViewSet from authentik.providers.proxy.api import ProxyOutpostConfigViewSet, ProxyProviderViewSet
from authentik.providers.radius.api import RadiusOutpostConfigViewSet, RadiusProviderViewSet
from authentik.providers.saml.api.property_mapping import SAMLPropertyMappingViewSet from authentik.providers.saml.api.property_mapping import SAMLPropertyMappingViewSet
from authentik.providers.saml.api.providers import SAMLProviderViewSet from authentik.providers.saml.api.providers import SAMLProviderViewSet
from authentik.providers.scim.api.property_mapping import SCIMMappingViewSet from authentik.providers.scim.api.property_mapping import SCIMMappingViewSet
@ -129,7 +128,6 @@ router.register("outposts/service_connections/docker", DockerServiceConnectionVi
router.register("outposts/service_connections/kubernetes", KubernetesServiceConnectionViewSet) router.register("outposts/service_connections/kubernetes", KubernetesServiceConnectionViewSet)
router.register("outposts/proxy", ProxyOutpostConfigViewSet) router.register("outposts/proxy", ProxyOutpostConfigViewSet)
router.register("outposts/ldap", LDAPOutpostConfigViewSet) router.register("outposts/ldap", LDAPOutpostConfigViewSet)
router.register("outposts/radius", RadiusOutpostConfigViewSet)
router.register("flows/instances", FlowViewSet) router.register("flows/instances", FlowViewSet)
router.register("flows/bindings", FlowStageBindingViewSet) router.register("flows/bindings", FlowStageBindingViewSet)
@ -168,7 +166,6 @@ router.register("providers/proxy", ProxyProviderViewSet)
router.register("providers/oauth2", OAuth2ProviderViewSet) router.register("providers/oauth2", OAuth2ProviderViewSet)
router.register("providers/saml", SAMLProviderViewSet) router.register("providers/saml", SAMLProviderViewSet)
router.register("providers/scim", SCIMProviderViewSet) router.register("providers/scim", SCIMProviderViewSet)
router.register("providers/radius", RadiusProviderViewSet)
router.register("oauth2/authorization_codes", AuthorizationCodeViewSet) router.register("oauth2/authorization_codes", AuthorizationCodeViewSet)
router.register("oauth2/refresh_tokens", RefreshTokenViewSet) router.register("oauth2/refresh_tokens", RefreshTokenViewSet)

View File

@ -19,8 +19,10 @@ class Command(BaseCommand):
for blueprint_path in options.get("blueprints", []): for blueprint_path in options.get("blueprints", []):
content = BlueprintInstance(path=blueprint_path).retrieve() content = BlueprintInstance(path=blueprint_path).retrieve()
importer = Importer(content) importer = Importer(content)
valid, _ = importer.validate() valid, logs = importer.validate()
if not valid: if not valid:
for log in logs:
getattr(LOGGER, log.pop("log_level"))(**log)
self.stderr.write("blueprint invalid") self.stderr.write("blueprint invalid")
sys_exit(1) sys_exit(1)
importer.apply() importer.apply()

View File

@ -82,10 +82,7 @@ class BlueprintInstance(SerializerModel, ManagedModel, CreatedUpdatedModel):
def retrieve_file(self) -> str: def retrieve_file(self) -> str:
"""Get blueprint from path""" """Get blueprint from path"""
try: try:
base = Path(CONFIG.y("blueprints_dir")) full_path = Path(CONFIG.y("blueprints_dir")).joinpath(Path(self.path))
full_path = base.joinpath(Path(self.path)).resolve()
if not str(full_path).startswith(str(base.resolve())):
raise BlueprintRetrievalFailed("Invalid blueprint path")
with full_path.open("r", encoding="utf-8") as _file: with full_path.open("r", encoding="utf-8") as _file:
return _file.read() return _file.read()
except (IOError, OSError) as exc: except (IOError, OSError) as exc:

View File

@ -1,15 +1,34 @@
"""authentik managed models tests""" """authentik managed models tests"""
from typing import Callable, Type
from django.apps import apps
from django.test import TestCase from django.test import TestCase
from authentik.blueprints.models import BlueprintInstance, BlueprintRetrievalFailed from authentik.blueprints.v1.importer import is_model_allowed
from authentik.lib.generators import generate_id from authentik.lib.models import SerializerModel
class TestModels(TestCase): class TestModels(TestCase):
"""Test Models""" """Test Models"""
def test_retrieve_file(self):
"""Test retrieve_file""" def serializer_tester_factory(test_model: Type[SerializerModel]) -> Callable:
instance = BlueprintInstance.objects.create(name=generate_id(), path="../etc/hosts") """Test serializer"""
with self.assertRaises(BlueprintRetrievalFailed):
instance.retrieve() def tester(self: TestModels):
if test_model._meta.abstract: # pragma: no cover
return
model_class = test_model()
self.assertTrue(isinstance(model_class, SerializerModel))
self.assertIsNotNone(model_class.serializer)
return tester
for app in apps.get_app_configs():
if not app.label.startswith("authentik"):
continue
for model in app.get_models():
if not is_model_allowed(model):
continue
setattr(TestModels, f"test_{app.label}_{model.__name__}", serializer_tester_factory(model))

View File

@ -1,34 +0,0 @@
"""authentik managed models tests"""
from typing import Callable, Type
from django.apps import apps
from django.test import TestCase
from authentik.blueprints.v1.importer import is_model_allowed
from authentik.lib.models import SerializerModel
class TestModels(TestCase):
"""Test Models"""
def serializer_tester_factory(test_model: Type[SerializerModel]) -> Callable:
"""Test serializer"""
def tester(self: TestModels):
if test_model._meta.abstract: # pragma: no cover
return
model_class = test_model()
self.assertTrue(isinstance(model_class, SerializerModel))
self.assertIsNotNone(model_class.serializer)
return tester
for app in apps.get_app_configs():
if not app.label.startswith("authentik"):
continue
for model in app.get_models():
if not is_model_allowed(model):
continue
setattr(TestModels, f"test_{app.label}_{model.__name__}", serializer_tester_factory(model))

View File

@ -40,10 +40,6 @@ from authentik.lib.models import SerializerModel
from authentik.outposts.models import OutpostServiceConnection from authentik.outposts.models import OutpostServiceConnection
from authentik.policies.models import Policy, PolicyBindingModel from authentik.policies.models import Policy, PolicyBindingModel
# Context set when the serializer is created in a blueprint context
# Update website/developer-docs/blueprints/v1/models.md when used
SERIALIZER_CONTEXT_BLUEPRINT = "blueprint_entry"
def is_model_allowed(model: type[Model]) -> bool: def is_model_allowed(model: type[Model]) -> bool:
"""Check if model is allowed""" """Check if model is allowed"""
@ -162,12 +158,7 @@ class Importer:
raise EntryInvalidError(f"Model {model} not allowed") raise EntryInvalidError(f"Model {model} not allowed")
if issubclass(model, BaseMetaModel): if issubclass(model, BaseMetaModel):
serializer_class: type[Serializer] = model.serializer() serializer_class: type[Serializer] = model.serializer()
serializer = serializer_class( serializer = serializer_class(data=entry.get_attrs(self.__import))
data=entry.get_attrs(self.__import),
context={
SERIALIZER_CONTEXT_BLUEPRINT: entry,
},
)
try: try:
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
except ValidationError as exc: except ValidationError as exc:
@ -226,12 +217,7 @@ class Importer:
always_merger.merge(full_data, updated_identifiers) always_merger.merge(full_data, updated_identifiers)
serializer_kwargs["data"] = full_data serializer_kwargs["data"] = full_data
serializer: Serializer = model().serializer( serializer: Serializer = model().serializer(**serializer_kwargs)
context={
SERIALIZER_CONTEXT_BLUEPRINT: entry,
},
**serializer_kwargs,
)
try: try:
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
except ValidationError as exc: except ValidationError as exc:

View File

@ -122,7 +122,7 @@ def blueprints_find():
) )
blueprint.meta = from_dict(BlueprintMetadata, metadata) if metadata else None blueprint.meta = from_dict(BlueprintMetadata, metadata) if metadata else None
blueprints.append(blueprint) blueprints.append(blueprint)
LOGGER.debug( LOGGER.info(
"parsed & loaded blueprint", "parsed & loaded blueprint",
hash=file_hash, hash=file_hash,
path=str(path), path=str(path),

View File

@ -35,7 +35,6 @@ class ProviderSerializer(ModelSerializer, MetaNameSerializer):
fields = [ fields = [
"pk", "pk",
"name", "name",
"authentication_flow",
"authorization_flow", "authorization_flow",
"property_mappings", "property_mappings",
"component", "component",

View File

@ -16,7 +16,6 @@ from rest_framework.viewsets import ModelViewSet
from authentik.api.authorization import OwnerSuperuserPermissions from authentik.api.authorization import OwnerSuperuserPermissions
from authentik.api.decorators import permission_required from authentik.api.decorators import permission_required
from authentik.blueprints.api import ManagedSerializer from authentik.blueprints.api import ManagedSerializer
from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT
from authentik.core.api.used_by import UsedByMixin from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.users import UserSerializer from authentik.core.api.users import UserSerializer
from authentik.core.api.utils import PassiveSerializer from authentik.core.api.utils import PassiveSerializer
@ -30,11 +29,6 @@ class TokenSerializer(ManagedSerializer, ModelSerializer):
user_obj = UserSerializer(required=False, source="user", read_only=True) user_obj = UserSerializer(required=False, source="user", read_only=True)
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
if SERIALIZER_CONTEXT_BLUEPRINT in self.context:
self.fields["key"] = CharField()
def validate(self, attrs: dict[Any, str]) -> dict[Any, str]: def validate(self, attrs: dict[Any, str]) -> dict[Any, str]:
"""Ensure only API or App password tokens are created.""" """Ensure only API or App password tokens are created."""
request: Request = self.context.get("request") request: Request = self.context.get("request")

View File

@ -67,12 +67,11 @@ from authentik.core.models import (
TokenIntents, TokenIntents,
User, User,
) )
from authentik.events.models import Event, EventAction from authentik.events.models import EventAction
from authentik.flows.exceptions import FlowNonApplicableException from authentik.flows.exceptions import FlowNonApplicableException
from authentik.flows.models import FlowToken from authentik.flows.models import FlowToken
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner
from authentik.flows.views.executor import QS_KEY_TOKEN from authentik.flows.views.executor import QS_KEY_TOKEN
from authentik.lib.config import CONFIG
from authentik.stages.email.models import EmailStage from authentik.stages.email.models import EmailStage
from authentik.stages.email.tasks import send_mails from authentik.stages.email.tasks import send_mails
from authentik.stages.email.utils import TemplateEmailMessage from authentik.stages.email.utils import TemplateEmailMessage
@ -212,9 +211,8 @@ class UserMetricsSerializer(PassiveSerializer):
def get_logins(self, _): def get_logins(self, _):
"""Get successful logins per 8 hours for the last 7 days""" """Get successful logins per 8 hours for the last 7 days"""
user = self.context["user"] user = self.context["user"]
request = self.context["request"]
return ( return (
get_objects_for_user(request.user, "authentik_events.view_event").filter( get_objects_for_user(user, "authentik_events.view_event").filter(
action=EventAction.LOGIN, user__pk=user.pk action=EventAction.LOGIN, user__pk=user.pk
) )
# 3 data points per day, so 8 hour spans # 3 data points per day, so 8 hour spans
@ -225,9 +223,8 @@ class UserMetricsSerializer(PassiveSerializer):
def get_logins_failed(self, _): def get_logins_failed(self, _):
"""Get failed logins per 8 hours for the last 7 days""" """Get failed logins per 8 hours for the last 7 days"""
user = self.context["user"] user = self.context["user"]
request = self.context["request"]
return ( return (
get_objects_for_user(request.user, "authentik_events.view_event").filter( get_objects_for_user(user, "authentik_events.view_event").filter(
action=EventAction.LOGIN_FAILED, context__username=user.username action=EventAction.LOGIN_FAILED, context__username=user.username
) )
# 3 data points per day, so 8 hour spans # 3 data points per day, so 8 hour spans
@ -238,9 +235,8 @@ class UserMetricsSerializer(PassiveSerializer):
def get_authorizations(self, _): def get_authorizations(self, _):
"""Get failed logins per 8 hours for the last 7 days""" """Get failed logins per 8 hours for the last 7 days"""
user = self.context["user"] user = self.context["user"]
request = self.context["request"]
return ( return (
get_objects_for_user(request.user, "authentik_events.view_event").filter( get_objects_for_user(user, "authentik_events.view_event").filter(
action=EventAction.AUTHORIZE_APPLICATION, user__pk=user.pk action=EventAction.AUTHORIZE_APPLICATION, user__pk=user.pk
) )
# 3 data points per day, so 8 hour spans # 3 data points per day, so 8 hour spans
@ -475,9 +471,8 @@ class UserViewSet(UsedByMixin, ModelViewSet):
def metrics(self, request: Request, pk: int) -> Response: def metrics(self, request: Request, pk: int) -> Response:
"""User metrics per 1h""" """User metrics per 1h"""
user: User = self.get_object() user: User = self.get_object()
serializer = UserMetricsSerializer(instance={}) serializer = UserMetricsSerializer(True)
serializer.context["user"] = user serializer.context["user"] = user
serializer.context["request"] = request
return Response(serializer.data) return Response(serializer.data)
@permission_required("authentik_core.reset_user_password") @permission_required("authentik_core.reset_user_password")
@ -544,58 +539,6 @@ class UserViewSet(UsedByMixin, ModelViewSet):
send_mails(email_stage, message) send_mails(email_stage, message)
return Response(status=204) return Response(status=204)
@permission_required("authentik_core.impersonate")
@extend_schema(
request=OpenApiTypes.NONE,
responses={
"204": OpenApiResponse(description="Successfully started impersonation"),
"401": OpenApiResponse(description="Access denied"),
},
)
@action(detail=True, methods=["POST"])
def impersonate(self, request: Request, pk: int) -> Response:
"""Impersonate a user"""
if not CONFIG.y_bool("impersonation"):
LOGGER.debug("User attempted to impersonate", user=request.user)
return Response(status=401)
if not request.user.has_perm("impersonate"):
LOGGER.debug("User attempted to impersonate without permissions", user=request.user)
return Response(status=401)
user_to_be = self.get_object()
request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER] = request.user
request.session[SESSION_KEY_IMPERSONATE_USER] = user_to_be
Event.new(EventAction.IMPERSONATION_STARTED).from_http(request, user_to_be)
return Response(status=201)
@extend_schema(
request=OpenApiTypes.NONE,
responses={
"204": OpenApiResponse(description="Successfully started impersonation"),
},
)
@action(detail=False, methods=["GET"])
def impersonate_end(self, request: Request) -> Response:
"""End Impersonation a user"""
if (
SESSION_KEY_IMPERSONATE_USER not in request.session
or SESSION_KEY_IMPERSONATE_ORIGINAL_USER not in request.session
):
LOGGER.debug("Can't end impersonation", user=request.user)
return Response(status=204)
original_user = request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER]
del request.session[SESSION_KEY_IMPERSONATE_USER]
del request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER]
Event.new(EventAction.IMPERSONATION_ENDED).from_http(request, original_user)
return Response(status=204)
def _filter_queryset_for_list(self, queryset: QuerySet) -> QuerySet: def _filter_queryset_for_list(self, queryset: QuerySet) -> QuerySet:
"""Custom filter_queryset method which ignores guardian, but still supports sorting""" """Custom filter_queryset method which ignores guardian, but still supports sorting"""
for backend in list(self.filter_backends): for backend in list(self.filter_backends):

View File

@ -11,7 +11,6 @@ class AuthentikCoreConfig(ManagedAppConfig):
label = "authentik_core" label = "authentik_core"
verbose_name = "authentik Core" verbose_name = "authentik Core"
mountpoint = "" mountpoint = ""
ws_mountpoint = "authentik.core.urls"
default = True default = True
def reconcile_load_core_signals(self): def reconcile_load_core_signals(self):

View File

@ -21,14 +21,11 @@ PROPERTY_MAPPING_TIME = Histogram(
class PropertyMappingEvaluator(BaseEvaluator): class PropertyMappingEvaluator(BaseEvaluator):
"""Custom Evaluator that adds some different context variables.""" """Custom Evaluator that adds some different context variables."""
dry_run: bool
def __init__( def __init__(
self, self,
model: Model, model: Model,
user: Optional[User] = None, user: Optional[User] = None,
request: Optional[HttpRequest] = None, request: Optional[HttpRequest] = None,
dry_run: Optional[bool] = False,
**kwargs, **kwargs,
): ):
if hasattr(model, "name"): if hasattr(model, "name"):
@ -45,13 +42,9 @@ class PropertyMappingEvaluator(BaseEvaluator):
req.http_request = request req.http_request = request
self._context["request"] = req self._context["request"] = req
self._context.update(**kwargs) self._context.update(**kwargs)
self.dry_run = dry_run
def handle_error(self, exc: Exception, expression_source: str): def handle_error(self, exc: Exception, expression_source: str):
"""Exception Handler""" """Exception Handler"""
# For dry-run requests we don't save exceptions
if self.dry_run:
return
error_string = exception_to_string(exc) error_string = exception_to_string(exc)
event = Event.new( event = Event.new(
EventAction.PROPERTY_MAPPING_EXCEPTION, EventAction.PROPERTY_MAPPING_EXCEPTION,

View File

@ -1,19 +0,0 @@
# Generated by Django 4.1.7 on 2023-03-19 21:57
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_core", "0026_alter_propertymapping_name_alter_provider_name"),
]
operations = [
migrations.AlterField(
model_name="user",
name="uuid",
field=models.UUIDField(default=uuid.uuid4, editable=False, unique=True),
),
]

View File

@ -1,25 +0,0 @@
# Generated by Django 4.1.7 on 2023-03-23 21:44
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_flows", "0025_alter_flowstagebinding_evaluate_on_plan_and_more"),
("authentik_core", "0027_alter_user_uuid"),
]
operations = [
migrations.AddField(
model_name="provider",
name="authentication_flow",
field=models.ForeignKey(
help_text="Flow used for authentication when the associated application is accessed by an un-authenticated user.",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="provider_authentication",
to="authentik_flows.flow",
),
),
]

View File

@ -146,7 +146,7 @@ class UserManager(DjangoUserManager):
class User(SerializerModel, GuardianUserMixin, AbstractUser): class User(SerializerModel, GuardianUserMixin, AbstractUser):
"""Custom User model to allow easier adding of user-based settings""" """Custom User model to allow easier adding of user-based settings"""
uuid = models.UUIDField(default=uuid4, editable=False, unique=True) uuid = models.UUIDField(default=uuid4, editable=False)
name = models.TextField(help_text=_("User's display name.")) name = models.TextField(help_text=_("User's display name."))
path = models.TextField(default="users") path = models.TextField(default="users")
@ -249,17 +249,6 @@ class Provider(SerializerModel):
name = models.TextField(unique=True) name = models.TextField(unique=True)
authentication_flow = models.ForeignKey(
"authentik_flows.Flow",
null=True,
on_delete=models.SET_NULL,
help_text=_(
"Flow used for authentication when the associated application is accessed by an "
"un-authenticated user."
),
related_name="provider_authentication",
)
authorization_flow = models.ForeignKey( authorization_flow = models.ForeignKey(
"authentik_flows.Flow", "authentik_flows.Flow",
on_delete=models.CASCADE, on_delete=models.CASCADE,

View File

@ -9,13 +9,16 @@
<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:tenant.branding_title %}{% endblock %}</title> <title>{% block title %}{% trans title|default:tenant.branding_title %}{% endblock %}</title>
<link rel="shortcut icon" type="image/png" href="{% static 'dist/assets/icons/icon.png' %}"> <link rel="shortcut icon" type="image/png" href="{% static 'dist/assets/icons/icon.png' %}">
<link rel="stylesheet" type="text/css" href="{% static 'dist/patternfly-base.css' %}">
<link rel="stylesheet" type="text/css" href="{% static 'dist/page.css' %}">
<link rel="stylesheet" type="text/css" href="{% static 'dist/empty-state.css' %}">
<link rel="stylesheet" type="text/css" href="{% static 'dist/spinner.css' %}">
{% block head_before %} {% block head_before %}
{% endblock %} {% endblock %}
<link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}"> <link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}">
<link rel="stylesheet" type="text/css" href="{% static 'dist/theme-dark.css' %}" media="(prefers-color-scheme: dark)"> <link rel="stylesheet" type="text/css" href="{% static 'dist/theme-dark.css' %}" media="(prefers-color-scheme: dark)">
<link rel="stylesheet" type="text/css" href="{% static 'dist/custom.css' %}" data-inject> <link rel="stylesheet" type="text/css" href="{% static 'dist/custom.css' %}" data-inject>
<script src="{% static 'dist/poly.js' %}?version={{ version }}" type="module"></script> <script src="{% static 'dist/poly.js' %}" type="module"></script>
<script src="{% static 'dist/standalone/loading/index.js' %}?version={{ version }}" type="module"></script>
{% block head %} {% block head %}
{% endblock %} {% endblock %}
<meta name="sentry-trace" content="{{ sentry_trace }}" /> <meta name="sentry-trace" content="{{ sentry_trace }}" />

View File

@ -1,6 +1,7 @@
{% extends "base/skeleton.html" %} {% extends "base/skeleton.html" %}
{% load static %} {% load static %}
{% load i18n %}
{% block head %} {% block head %}
<script src="{% static 'dist/admin/AdminInterface.js' %}?version={{ version }}" type="module"></script> <script src="{% static 'dist/admin/AdminInterface.js' %}?version={{ version }}" type="module"></script>
@ -14,6 +15,19 @@
{% 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> <section class="ak-static-page pf-c-page__main-section pf-m-no-padding-mobile pf-m-xl">
<div class="pf-c-empty-state" style="height: 100vh;">
<div class="pf-c-empty-state__content">
<span class="pf-c-spinner pf-m-xl pf-c-empty-state__icon" role="progressbar" aria-valuetext="{% trans 'Loading...' %}">
<span class="pf-c-spinner__clipper"></span>
<span class="pf-c-spinner__lead-ball"></span>
<span class="pf-c-spinner__tail-ball"></span>
</span>
<h1 class="pf-c-title pf-m-lg">
{% trans "Loading..." %}
</h1>
</div>
</div>
</section>
</ak-interface-admin> </ak-interface-admin>
{% endblock %} {% endblock %}

View File

@ -1,6 +1,7 @@
{% extends "base/skeleton.html" %} {% extends "base/skeleton.html" %}
{% load static %} {% load static %}
{% load i18n %}
{% block head_before %} {% block head_before %}
{{ block.super }} {{ block.super }}
@ -30,6 +31,19 @@ window.authentik.flow = {
{% block body %} {% block body %}
<ak-message-container></ak-message-container> <ak-message-container></ak-message-container>
<ak-flow-executor> <ak-flow-executor>
<ak-loading></ak-loading> <section class="ak-static-page pf-c-page__main-section pf-m-no-padding-mobile pf-m-xl">
<div class="pf-c-empty-state" style="height: 100vh;">
<div class="pf-c-empty-state__content">
<span class="pf-c-spinner pf-m-xl pf-c-empty-state__icon" role="progressbar" aria-valuetext="{% trans 'Loading...' %}">
<span class="pf-c-spinner__clipper"></span>
<span class="pf-c-spinner__lead-ball"></span>
<span class="pf-c-spinner__tail-ball"></span>
</span>
<h1 class="pf-c-title pf-m-lg">
{% trans "Loading..." %}
</h1>
</div>
</div>
</section>
</ak-flow-executor> </ak-flow-executor>
{% endblock %} {% endblock %}

View File

@ -1,6 +1,7 @@
{% extends "base/skeleton.html" %} {% extends "base/skeleton.html" %}
{% load static %} {% load static %}
{% load i18n %}
{% block head %} {% block head %}
<script src="{% static 'dist/user/UserInterface.js' %}?version={{ version }}" type="module"></script> <script src="{% static 'dist/user/UserInterface.js' %}?version={{ version }}" type="module"></script>
@ -14,6 +15,19 @@
{% 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> <section class="ak-static-page pf-c-page__main-section pf-m-no-padding-mobile pf-m-xl">
<div class="pf-c-empty-state" style="height: 100vh;">
<div class="pf-c-empty-state__content">
<span class="pf-c-spinner pf-m-xl pf-c-empty-state__icon" role="progressbar" aria-valuetext="{% trans 'Loading...' %}">
<span class="pf-c-spinner__clipper"></span>
<span class="pf-c-spinner__lead-ball"></span>
<span class="pf-c-spinner__tail-ball"></span>
</span>
<h1 class="pf-c-title pf-m-lg">
{% trans "Loading..." %}
</h1>
</div>
</div>
</section>
</ak-interface-user> </ak-interface-user>
{% endblock %} {% endblock %}

View File

@ -129,7 +129,6 @@ class TestApplicationsAPI(APITestCase):
"provider_obj": { "provider_obj": {
"assigned_application_name": "allowed", "assigned_application_name": "allowed",
"assigned_application_slug": "allowed", "assigned_application_slug": "allowed",
"authentication_flow": None,
"authorization_flow": str(self.provider.authorization_flow.pk), "authorization_flow": str(self.provider.authorization_flow.pk),
"component": "ak-provider-oauth2-form", "component": "ak-provider-oauth2-form",
"meta_model_name": "authentik_providers_oauth2.oauth2provider", "meta_model_name": "authentik_providers_oauth2.oauth2provider",
@ -179,7 +178,6 @@ class TestApplicationsAPI(APITestCase):
"provider_obj": { "provider_obj": {
"assigned_application_name": "allowed", "assigned_application_name": "allowed",
"assigned_application_slug": "allowed", "assigned_application_slug": "allowed",
"authentication_flow": None,
"authorization_flow": str(self.provider.authorization_flow.pk), "authorization_flow": str(self.provider.authorization_flow.pk),
"component": "ak-provider-oauth2-form", "component": "ak-provider-oauth2-form",
"meta_model_name": "authentik_providers_oauth2.oauth2provider", "meta_model_name": "authentik_providers_oauth2.oauth2provider",

View File

@ -1,14 +1,14 @@
"""impersonation tests""" """impersonation tests"""
from json import loads from json import loads
from django.test.testcases import TestCase
from django.urls import reverse from django.urls import reverse
from rest_framework.test import APITestCase
from authentik.core.models import 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
class TestImpersonation(APITestCase): class TestImpersonation(TestCase):
"""impersonation tests""" """impersonation tests"""
def setUp(self) -> None: def setUp(self) -> None:
@ -23,10 +23,10 @@ class TestImpersonation(APITestCase):
self.other_user.save() self.other_user.save()
self.client.force_login(self.user) self.client.force_login(self.user)
self.client.post( self.client.get(
reverse( reverse(
"authentik_api:user-impersonate", "authentik_core:impersonate-init",
kwargs={"pk": self.other_user.pk}, kwargs={"user_id": self.other_user.pk},
) )
) )
@ -35,7 +35,7 @@ class TestImpersonation(APITestCase):
self.assertEqual(response_body["user"]["username"], self.other_user.username) self.assertEqual(response_body["user"]["username"], self.other_user.username)
self.assertEqual(response_body["original"]["username"], self.user.username) self.assertEqual(response_body["original"]["username"], self.user.username)
self.client.get(reverse("authentik_api:user-impersonate-end")) self.client.get(reverse("authentik_core:impersonate-end"))
response = self.client.get(reverse("authentik_api:user-me")) response = self.client.get(reverse("authentik_api:user-me"))
response_body = loads(response.content.decode()) response_body = loads(response.content.decode())
@ -46,7 +46,9 @@ class TestImpersonation(APITestCase):
"""test impersonation without permissions""" """test impersonation without permissions"""
self.client.force_login(self.other_user) self.client.force_login(self.other_user)
self.client.get(reverse("authentik_api:user-impersonate", kwargs={"pk": self.user.pk})) self.client.get(
reverse("authentik_core:impersonate-init", kwargs={"user_id": self.user.pk})
)
response = self.client.get(reverse("authentik_api:user-me")) response = self.client.get(reverse("authentik_api:user-me"))
response_body = loads(response.content.decode()) response_body = loads(response.content.decode())
@ -56,5 +58,5 @@ class TestImpersonation(APITestCase):
"""test un-impersonation without impersonating first""" """test un-impersonation without impersonating first"""
self.client.force_login(self.other_user) self.client.force_login(self.other_user)
response = self.client.get(reverse("authentik_api:user-impersonate-end")) response = self.client.get(reverse("authentik_core:impersonate-end"))
self.assertEqual(response.status_code, 204) self.assertRedirects(response, reverse("authentik_core:if-user"))

View File

@ -4,10 +4,7 @@ from guardian.shortcuts import get_anonymous_user
from authentik.core.exceptions import PropertyMappingExpressionException from authentik.core.exceptions import PropertyMappingExpressionException
from authentik.core.models import PropertyMapping from authentik.core.models import PropertyMapping
from authentik.core.tests.utils import create_test_admin_user
from authentik.events.models import Event, EventAction from authentik.events.models import Event, EventAction
from authentik.lib.generators import generate_id
from authentik.policies.expression.models import ExpressionPolicy
class TestPropertyMappings(TestCase): class TestPropertyMappings(TestCase):
@ -15,24 +12,23 @@ class TestPropertyMappings(TestCase):
def setUp(self) -> None: def setUp(self) -> None:
super().setUp() super().setUp()
self.user = create_test_admin_user()
self.factory = RequestFactory() self.factory = RequestFactory()
def test_expression(self): def test_expression(self):
"""Test expression""" """Test expression"""
mapping = PropertyMapping.objects.create(name=generate_id(), expression="return 'test'") mapping = PropertyMapping.objects.create(name="test", expression="return 'test'")
self.assertEqual(mapping.evaluate(None, None), "test") self.assertEqual(mapping.evaluate(None, None), "test")
def test_expression_syntax(self): def test_expression_syntax(self):
"""Test expression syntax error""" """Test expression syntax error"""
mapping = PropertyMapping.objects.create(name=generate_id(), expression="-") mapping = PropertyMapping.objects.create(name="test", expression="-")
with self.assertRaises(PropertyMappingExpressionException): with self.assertRaises(PropertyMappingExpressionException):
mapping.evaluate(None, None) mapping.evaluate(None, None)
def test_expression_error_general(self): def test_expression_error_general(self):
"""Test expression error""" """Test expression error"""
expr = "return aaa" expr = "return aaa"
mapping = PropertyMapping.objects.create(name=generate_id(), expression=expr) mapping = PropertyMapping.objects.create(name="test", expression=expr)
with self.assertRaises(PropertyMappingExpressionException): with self.assertRaises(PropertyMappingExpressionException):
mapping.evaluate(None, None) mapping.evaluate(None, None)
events = Event.objects.filter( events = Event.objects.filter(
@ -45,7 +41,7 @@ class TestPropertyMappings(TestCase):
"""Test expression error (with user and http request""" """Test expression error (with user and http request"""
expr = "return aaa" expr = "return aaa"
request = self.factory.get("/") request = self.factory.get("/")
mapping = PropertyMapping.objects.create(name=generate_id(), expression=expr) mapping = PropertyMapping.objects.create(name="test", expression=expr)
with self.assertRaises(PropertyMappingExpressionException): with self.assertRaises(PropertyMappingExpressionException):
mapping.evaluate(get_anonymous_user(), request) mapping.evaluate(get_anonymous_user(), request)
events = Event.objects.filter( events = Event.objects.filter(
@ -56,23 +52,3 @@ class TestPropertyMappings(TestCase):
event = events.first() event = events.first()
self.assertEqual(event.user["username"], "AnonymousUser") self.assertEqual(event.user["username"], "AnonymousUser")
self.assertEqual(event.client_ip, "127.0.0.1") self.assertEqual(event.client_ip, "127.0.0.1")
def test_call_policy(self):
"""test ak_call_policy"""
expr = ExpressionPolicy.objects.create(
name=generate_id(),
execution_logging=True,
expression="return request.http_request.path",
)
http_request = self.factory.get("/")
tmpl = (
"""
res = ak_call_policy('%s')
result = [request.http_request.path, res.raw_result]
return result
"""
% expr.name
)
evaluator = PropertyMapping(expression=tmpl, name=generate_id())
res = evaluator.evaluate(self.user, http_request)
self.assertEqual(res, ["/", "/"])

View File

@ -27,6 +27,6 @@ class UserSettingSerializer(PassiveSerializer):
object_uid = CharField() object_uid = CharField()
component = CharField() component = CharField()
title = CharField(required=True) title = CharField()
configure_url = CharField(required=False) configure_url = CharField(required=False)
icon_url = CharField(required=False) icon_url = CharField(required=False)

View File

@ -1,18 +1,14 @@
"""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
from django.views.decorators.csrf import ensure_csrf_cookie from django.views.decorators.csrf import ensure_csrf_cookie
from django.views.generic import RedirectView from django.views.generic import RedirectView
from authentik.core.views import apps from authentik.core.views import apps, impersonate
from authentik.core.views.debug import AccessDeniedView from authentik.core.views.debug import AccessDeniedView
from authentik.core.views.interface import FlowInterfaceView, InterfaceView from authentik.core.views.interface import FlowInterfaceView, InterfaceView
from authentik.core.views.session import EndSessionView from authentik.core.views.session import EndSessionView
from authentik.root.asgi_middleware import SessionMiddleware
from authentik.root.messages.consumer import MessageConsumer
urlpatterns = [ urlpatterns = [
path( path(
@ -28,6 +24,17 @@ urlpatterns = [
apps.RedirectToAppLaunch.as_view(), apps.RedirectToAppLaunch.as_view(),
name="application-launch", name="application-launch",
), ),
# Impersonation
path(
"-/impersonation/<int:user_id>/",
impersonate.ImpersonateInitView.as_view(),
name="impersonate-init",
),
path(
"-/impersonation/end/",
impersonate.ImpersonateEndView.as_view(),
name="impersonate-end",
),
# Interfaces # Interfaces
path( path(
"if/admin/", "if/admin/",
@ -57,12 +64,6 @@ urlpatterns = [
), ),
] ]
websocket_urlpatterns = [
path(
"ws/client/", CookieMiddleware(SessionMiddleware(AuthMiddleware(MessageConsumer.as_asgi())))
),
]
if settings.DEBUG: if settings.DEBUG:
urlpatterns += [ urlpatterns += [
path("debug/policy/deny/", AccessDeniedView.as_view(), name="debug-policy-deny"), path("debug/policy/deny/", AccessDeniedView.as_view(), name="debug-policy-deny"),

View File

@ -12,19 +12,16 @@ from authentik.flows.challenge import (
RedirectChallenge, RedirectChallenge,
) )
from authentik.flows.exceptions import FlowNonApplicableException from authentik.flows.exceptions import FlowNonApplicableException
from authentik.flows.models import FlowDesignation, in_memory_stage from authentik.flows.models import in_memory_stage
from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, FlowPlanner from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, FlowPlanner
from authentik.flows.stage import ChallengeStageView from authentik.flows.stage import ChallengeStageView
from authentik.flows.views.executor import ( from authentik.flows.views.executor import SESSION_KEY_PLAN
SESSION_KEY_APPLICATION_PRE,
SESSION_KEY_PLAN,
ToDefaultFlow,
)
from authentik.lib.utils.urls import redirect_with_qs from authentik.lib.utils.urls import redirect_with_qs
from authentik.stages.consent.stage import ( from authentik.stages.consent.stage import (
PLAN_CONTEXT_CONSENT_HEADER, PLAN_CONTEXT_CONSENT_HEADER,
PLAN_CONTEXT_CONSENT_PERMISSIONS, PLAN_CONTEXT_CONSENT_PERMISSIONS,
) )
from authentik.tenants.models import Tenant
class RedirectToAppLaunch(View): class RedirectToAppLaunch(View):
@ -39,10 +36,10 @@ class RedirectToAppLaunch(View):
# Check if we're authenticated already, saves us the flow run # Check if we're authenticated already, saves us the flow run
if request.user.is_authenticated: if request.user.is_authenticated:
return HttpResponseRedirect(app.get_launch_url(request.user)) return HttpResponseRedirect(app.get_launch_url(request.user))
self.request.session[SESSION_KEY_APPLICATION_PRE] = app
# otherwise, do a custom flow plan that includes the application that's # otherwise, do a custom flow plan that includes the application that's
# being accessed, to improve usability # being accessed, to improve usability
flow = ToDefaultFlow(request=request, designation=FlowDesignation.AUTHENTICATION).get_flow() tenant: Tenant = request.tenant
flow = tenant.flow_authentication
planner = FlowPlanner(flow) planner = FlowPlanner(flow)
planner.allow_empty_flows = True planner.allow_empty_flows = True
try: try:

View File

@ -0,0 +1,60 @@
"""authentik impersonation views"""
from django.http import HttpRequest, HttpResponse
from django.shortcuts import get_object_or_404, redirect
from django.views import View
from structlog.stdlib import get_logger
from authentik.core.middleware import (
SESSION_KEY_IMPERSONATE_ORIGINAL_USER,
SESSION_KEY_IMPERSONATE_USER,
)
from authentik.core.models import User
from authentik.events.models import Event, EventAction
from authentik.lib.config import CONFIG
LOGGER = get_logger()
class ImpersonateInitView(View):
"""Initiate Impersonation"""
def get(self, request: HttpRequest, user_id: int) -> HttpResponse:
"""Impersonation handler, checks permissions"""
if not CONFIG.y_bool("impersonation"):
LOGGER.debug("User attempted to impersonate", user=request.user)
return HttpResponse("Unauthorized", status=401)
if not request.user.has_perm("impersonate"):
LOGGER.debug("User attempted to impersonate without permissions", user=request.user)
return HttpResponse("Unauthorized", status=401)
user_to_be = get_object_or_404(User, pk=user_id)
request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER] = request.user
request.session[SESSION_KEY_IMPERSONATE_USER] = user_to_be
Event.new(EventAction.IMPERSONATION_STARTED).from_http(request, user_to_be)
return redirect("authentik_core:if-user")
class ImpersonateEndView(View):
"""End User impersonation"""
def get(self, request: HttpRequest) -> HttpResponse:
"""End Impersonation handler"""
if (
SESSION_KEY_IMPERSONATE_USER not in request.session
or SESSION_KEY_IMPERSONATE_ORIGINAL_USER not in request.session
):
LOGGER.debug("Can't end impersonation", user=request.user)
return redirect("authentik_core:if-user")
original_user = request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER]
del request.session[SESSION_KEY_IMPERSONATE_USER]
del request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER]
Event.new(EventAction.IMPERSONATION_ENDED).from_http(request, original_user)
return redirect("authentik_core:root-redirect")

View File

@ -214,18 +214,11 @@ class Event(SerializerModel, ExpiringModel):
Events independently from requests. Events independently from requests.
`user` arguments optionally overrides user from requests.""" `user` arguments optionally overrides user from requests."""
if request: if request:
from authentik.flows.views.executor import QS_QUERY
self.context["http_request"] = { self.context["http_request"] = {
"path": request.path, "path": request.path,
"method": request.method, "method": request.method,
"args": QueryDict(request.META.get("QUERY_STRING", "")), "args": QueryDict(request.META.get("QUERY_STRING", "")),
} }
# Special case for events created during flow execution
# since they keep the http query within a wrapped query
if QS_QUERY in self.context["http_request"]["args"]:
wrapped = self.context["http_request"]["args"][QS_QUERY]
self.context["http_request"]["args"] = QueryDict(wrapped)
if hasattr(request, "tenant"): if hasattr(request, "tenant"):
tenant: Tenant = request.tenant tenant: Tenant = request.tenant
# Because self.created only gets set on save, we can't use it's value here # Because self.created only gets set on save, we can't use it's value here

View File

@ -23,8 +23,7 @@ class DiagramElement:
style: list[str] = field(default_factory=lambda: ["[", "]"]) style: list[str] = field(default_factory=lambda: ["[", "]"])
def __str__(self) -> str: def __str__(self) -> str:
description = self.description.replace('"', "#quot;") element = f'{self.identifier}{self.style[0]}"{self.description}"{self.style[1]}'
element = f'{self.identifier}{self.style[0]}"{description}"{self.style[1]}'
if self.action is not None: if self.action is not None:
if self.action != "": if self.action != "":
element = f"--{self.action}--> {element}" element = f"--{self.action}--> {element}"

View File

@ -271,15 +271,6 @@ class ConfigurableStage(models.Model):
abstract = True abstract = True
class FriendlyNamedStage(models.Model):
"""Abstract base class for a Stage that can have a user friendly name configured."""
friendly_name = models.TextField(null=True)
class Meta:
abstract = True
class FlowToken(Token): class FlowToken(Token):
"""Subclass of a standard Token, stores the currently active flow plan upon creation. """Subclass of a standard Token, stores the currently active flow plan upon creation.
Can be used to later resume a flow.""" Can be used to later resume a flow."""

View File

@ -204,12 +204,12 @@ class ChallengeStageView(StageView):
for field, errors in response.errors.items(): for field, errors in response.errors.items():
for error in errors: for error in errors:
full_errors.setdefault(field, []) full_errors.setdefault(field, [])
field_error = { full_errors[field].append(
{
"string": str(error), "string": str(error),
"code": error.code,
} }
if hasattr(error, "code"): )
field_error["code"] = error.code
full_errors[field].append(field_error)
challenge_response.initial_data["response_errors"] = full_errors challenge_response.initial_data["response_errors"] = full_errors
if not challenge_response.is_valid(): if not challenge_response.is_valid():
self.logger.error( self.logger.error(

View File

@ -2,13 +2,10 @@
from django.test import TestCase from django.test import TestCase
from django.urls import reverse from django.urls import reverse
from authentik.core.models import Application
from authentik.core.tests.utils import create_test_flow from authentik.core.tests.utils import create_test_flow
from authentik.flows.models import Flow, FlowDesignation from authentik.flows.models import Flow, FlowDesignation
from authentik.flows.planner import FlowPlan from authentik.flows.planner import FlowPlan
from authentik.flows.views.executor import SESSION_KEY_APPLICATION_PRE, SESSION_KEY_PLAN from authentik.flows.views.executor import SESSION_KEY_PLAN
from authentik.lib.generators import generate_id
from authentik.providers.oauth2.models import OAuth2Provider
class TestHelperView(TestCase): class TestHelperView(TestCase):
@ -25,41 +22,6 @@ class TestHelperView(TestCase):
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, expected_url) self.assertEqual(response.url, expected_url)
def test_default_view_app(self):
"""Test that ToDefaultFlow returns the expected URL (when accessing an application)"""
Flow.objects.filter(designation=FlowDesignation.AUTHENTICATION).delete()
flow = create_test_flow(FlowDesignation.AUTHENTICATION)
self.client.session[SESSION_KEY_APPLICATION_PRE] = Application(
name=generate_id(),
slug=generate_id(),
provider=OAuth2Provider(
name=generate_id(),
authentication_flow=flow,
),
)
response = self.client.get(
reverse("authentik_flows:default-authentication"),
)
expected_url = reverse("authentik_core:if-flow", kwargs={"flow_slug": flow.slug})
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, expected_url)
def test_default_view_app_no_provider(self):
"""Test that ToDefaultFlow returns the expected URL
(when accessing an application, without a provider)"""
Flow.objects.filter(designation=FlowDesignation.AUTHENTICATION).delete()
flow = create_test_flow(FlowDesignation.AUTHENTICATION)
self.client.session[SESSION_KEY_APPLICATION_PRE] = Application(
name=generate_id(),
slug=generate_id(),
)
response = self.client.get(
reverse("authentik_flows:default-authentication"),
)
expected_url = reverse("authentik_core:if-flow", kwargs={"flow_slug": flow.slug})
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, expected_url)
def test_default_view_invalid_plan(self): def test_default_view_invalid_plan(self):
"""Test that ToDefaultFlow returns the expected URL (with an invalid plan)""" """Test that ToDefaultFlow returns the expected URL (with an invalid plan)"""
Flow.objects.filter(designation=FlowDesignation.INVALIDATION).delete() Flow.objects.filter(designation=FlowDesignation.INVALIDATION).delete()

View File

@ -22,7 +22,6 @@ from sentry_sdk.api import set_tag
from sentry_sdk.hub import Hub from sentry_sdk.hub import Hub
from structlog.stdlib import BoundLogger, get_logger from structlog.stdlib import BoundLogger, get_logger
from authentik.core.models import Application
from authentik.events.models import Event, EventAction, cleanse_dict from authentik.events.models import Event, EventAction, cleanse_dict
from authentik.flows.challenge import ( from authentik.flows.challenge import (
Challenge, Challenge,
@ -69,7 +68,6 @@ SESSION_KEY_GET = "authentik/flows/get"
SESSION_KEY_POST = "authentik/flows/post" SESSION_KEY_POST = "authentik/flows/post"
SESSION_KEY_HISTORY = "authentik/flows/history" SESSION_KEY_HISTORY = "authentik/flows/history"
QS_KEY_TOKEN = "flow_token" # nosec QS_KEY_TOKEN = "flow_token" # nosec
QS_QUERY = "query"
def challenge_types(): def challenge_types():
@ -174,7 +172,7 @@ class FlowExecutorView(APIView):
op="authentik.flow.executor.dispatch", description=self.flow.slug op="authentik.flow.executor.dispatch", description=self.flow.slug
) as span: ) as span:
span.set_data("authentik Flow", self.flow.slug) span.set_data("authentik Flow", self.flow.slug)
get_params = QueryDict(request.GET.get(QS_QUERY, "")) get_params = QueryDict(request.GET.get("query", ""))
if QS_KEY_TOKEN in get_params: if QS_KEY_TOKEN in get_params:
plan = self._check_flow_token(get_params[QS_KEY_TOKEN]) plan = self._check_flow_token(get_params[QS_KEY_TOKEN])
if plan: if plan:
@ -477,32 +475,20 @@ class ToDefaultFlow(View):
LOGGER.debug("flow_by_policy: no flow found", filters=flow_filter) LOGGER.debug("flow_by_policy: no flow found", filters=flow_filter)
return None return None
def get_flow(self) -> Flow: def dispatch(self, request: HttpRequest) -> HttpResponse:
"""Get a flow for the selected designation""" tenant: Tenant = request.tenant
tenant: Tenant = self.request.tenant
flow = None flow = None
# First, attempt to get default flow from tenant # First, attempt to get default flow from tenant
if self.designation == FlowDesignation.AUTHENTICATION: if self.designation == FlowDesignation.AUTHENTICATION:
flow = tenant.flow_authentication flow = tenant.flow_authentication
# Check if we have a default flow from application if self.designation == FlowDesignation.INVALIDATION:
application: Optional[Application] = self.request.session.get(
SESSION_KEY_APPLICATION_PRE
)
if application and application.provider and application.provider.authentication_flow:
flow = application.provider.authentication_flow
elif self.designation == FlowDesignation.INVALIDATION:
flow = tenant.flow_invalidation flow = tenant.flow_invalidation
if flow:
return flow
# If no flow was set, get the first based on slug and policy # If no flow was set, get the first based on slug and policy
flow = self.flow_by_policy(self.request, designation=self.designation) if not flow:
if flow: flow = self.flow_by_policy(request, designation=self.designation)
return flow
# If we still don't have a flow, 404 # If we still don't have a flow, 404
if not flow:
raise Http404 raise Http404
def dispatch(self, request: HttpRequest) -> HttpResponse:
flow = self.get_flow()
# If user already has a pending plan, clear it so we don't have to later. # If user already has a pending plan, clear it so we don't have to later.
if SESSION_KEY_PLAN in self.request.session: if SESSION_KEY_PLAN in self.request.session:
plan: FlowPlan = self.request.session[SESSION_KEY_PLAN] plan: FlowPlan = self.request.session[SESSION_KEY_PLAN]

View File

@ -8,7 +8,6 @@ from typing import Any, Iterable, Optional
from cachetools import TLRUCache, cached from cachetools import TLRUCache, cached
from django.core.exceptions import FieldError from django.core.exceptions import FieldError
from django_otp import devices_for_user from django_otp import devices_for_user
from guardian.shortcuts import get_anonymous_user
from rest_framework.serializers import ValidationError from rest_framework.serializers import ValidationError
from sentry_sdk.hub import Hub from sentry_sdk.hub import Hub
from sentry_sdk.tracing import Span from sentry_sdk.tracing import Span
@ -17,9 +16,7 @@ from structlog.stdlib import get_logger
from authentik.core.models import User from authentik.core.models import User
from authentik.events.models import Event from authentik.events.models import Event
from authentik.lib.utils.http import get_http_session from authentik.lib.utils.http import get_http_session
from authentik.policies.models import Policy, PolicyBinding from authentik.policies.types import PolicyRequest
from authentik.policies.process import PolicyProcess
from authentik.policies.types import PolicyRequest, PolicyResult
LOGGER = get_logger() LOGGER = get_logger()
@ -40,20 +37,19 @@ class BaseEvaluator:
# update website/docs/expressions/_objects.md # update website/docs/expressions/_objects.md
# update website/docs/expressions/_functions.md # update website/docs/expressions/_functions.md
self._globals = { self._globals = {
"ak_call_policy": self.expr_func_call_policy,
"ak_create_event": self.expr_event_create,
"ak_is_group_member": BaseEvaluator.expr_is_group_member,
"ak_logger": get_logger(self._filename).bind(),
"ak_user_by": BaseEvaluator.expr_user_by,
"ak_user_has_authenticator": BaseEvaluator.expr_func_user_has_authenticator,
"ip_address": ip_address,
"ip_network": ip_network,
"list_flatten": BaseEvaluator.expr_flatten,
"regex_match": BaseEvaluator.expr_regex_match, "regex_match": BaseEvaluator.expr_regex_match,
"regex_replace": BaseEvaluator.expr_regex_replace, "regex_replace": BaseEvaluator.expr_regex_replace,
"requests": get_http_session(), "list_flatten": BaseEvaluator.expr_flatten,
"ak_is_group_member": BaseEvaluator.expr_is_group_member,
"ak_user_by": BaseEvaluator.expr_user_by,
"ak_user_has_authenticator": BaseEvaluator.expr_func_user_has_authenticator,
"resolve_dns": BaseEvaluator.expr_resolve_dns, "resolve_dns": BaseEvaluator.expr_resolve_dns,
"reverse_dns": BaseEvaluator.expr_reverse_dns, "reverse_dns": BaseEvaluator.expr_reverse_dns,
"ak_create_event": self.expr_event_create,
"ak_logger": get_logger(self._filename).bind(),
"requests": get_http_session(),
"ip_address": ip_address,
"ip_network": ip_network,
} }
self._context = {} self._context = {}
@ -156,19 +152,6 @@ class BaseEvaluator:
return return
event.save() event.save()
def expr_func_call_policy(self, name: str, **kwargs) -> PolicyResult:
"""Call policy by name, with current request"""
policy = Policy.objects.filter(name=name).select_subclasses().first()
if not policy:
raise ValueError(f"Policy '{name}' not found.")
user = self._context.get("user", get_anonymous_user())
req = PolicyRequest(user)
if "request" in self._context:
req = self._context["request"]
req.context.update(kwargs)
proc = PolicyProcess(PolicyBinding(policy=policy), request=req, connection=None)
return proc.profiling_wrapper()
def wrap_expression(self, expression: str, params: Iterable[str]) -> str: def wrap_expression(self, expression: str, params: Iterable[str]) -> str:
"""Wrap expression in a function, call it, and save the result as `result`""" """Wrap expression in a function, call it, and save the result as `result`"""
handler_signature = ",".join(params) handler_signature = ",".join(params)

View File

@ -19,12 +19,9 @@ from rest_framework.exceptions import APIException
from sentry_sdk import HttpTransport from sentry_sdk import HttpTransport
from sentry_sdk import init as sentry_sdk_init from sentry_sdk import init as sentry_sdk_init
from sentry_sdk.api import set_tag from sentry_sdk.api import set_tag
from sentry_sdk.integrations.argv import ArgvIntegration
from sentry_sdk.integrations.celery import CeleryIntegration from sentry_sdk.integrations.celery import CeleryIntegration
from sentry_sdk.integrations.django import DjangoIntegration from sentry_sdk.integrations.django import DjangoIntegration
from sentry_sdk.integrations.redis import RedisIntegration from sentry_sdk.integrations.redis import RedisIntegration
from sentry_sdk.integrations.socket import SocketIntegration
from sentry_sdk.integrations.stdlib import StdlibIntegration
from sentry_sdk.integrations.threading import ThreadingIntegration from sentry_sdk.integrations.threading import ThreadingIntegration
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from websockets.exceptions import WebSocketException from websockets.exceptions import WebSocketException
@ -64,13 +61,10 @@ def sentry_init(**sentry_init_kwargs):
sentry_sdk_init( sentry_sdk_init(
dsn=CONFIG.y("error_reporting.sentry_dsn"), dsn=CONFIG.y("error_reporting.sentry_dsn"),
integrations=[ integrations=[
ArgvIntegration(),
StdlibIntegration(),
DjangoIntegration(transaction_style="function_name"), DjangoIntegration(transaction_style="function_name"),
CeleryIntegration(monitor_beat_tasks=True), CeleryIntegration(),
RedisIntegration(), RedisIntegration(),
ThreadingIntegration(propagate_hub=True), ThreadingIntegration(propagate_hub=True),
SocketIntegration(),
], ],
before_send=before_send, before_send=before_send,
traces_sampler=traces_sampler, traces_sampler=traces_sampler,

View File

@ -28,7 +28,6 @@ from authentik.outposts.models import (
) )
from authentik.providers.ldap.models import LDAPProvider from authentik.providers.ldap.models import LDAPProvider
from authentik.providers.proxy.models import ProxyProvider from authentik.providers.proxy.models import ProxyProvider
from authentik.providers.radius.models import RadiusProvider
class OutpostSerializer(ModelSerializer): class OutpostSerializer(ModelSerializer):
@ -52,7 +51,6 @@ class OutpostSerializer(ModelSerializer):
type_map = { type_map = {
OutpostType.LDAP: LDAPProvider, OutpostType.LDAP: LDAPProvider,
OutpostType.PROXY: ProxyProvider, OutpostType.PROXY: ProxyProvider,
OutpostType.RADIUS: RadiusProvider,
None: Provider, None: Provider,
} }
for provider in providers: for provider in providers:

View File

@ -24,7 +24,6 @@ class AuthentikOutpostConfig(ManagedAppConfig):
label = "authentik_outposts" label = "authentik_outposts"
verbose_name = "authentik Outpost" verbose_name = "authentik Outpost"
default = True default = True
ws_mountpoint = "authentik.outposts.urls"
def reconcile_load_outposts_signals(self): def reconcile_load_outposts_signals(self):
"""Load outposts signals""" """Load outposts signals"""

View File

@ -4,7 +4,6 @@ from typing import TYPE_CHECKING
from django.utils.text import slugify from django.utils.text import slugify
from kubernetes.client import ( from kubernetes.client import (
AppsV1Api, AppsV1Api,
V1Capabilities,
V1Container, V1Container,
V1ContainerPort, V1ContainerPort,
V1Deployment, V1Deployment,
@ -14,12 +13,9 @@ from kubernetes.client import (
V1LabelSelector, V1LabelSelector,
V1ObjectMeta, V1ObjectMeta,
V1ObjectReference, V1ObjectReference,
V1PodSecurityContext,
V1PodSpec, V1PodSpec,
V1PodTemplateSpec, V1PodTemplateSpec,
V1SeccompProfile,
V1SecretKeySelector, V1SecretKeySelector,
V1SecurityContext,
) )
from authentik import __version__, get_full_version from authentik import __version__, get_full_version
@ -107,11 +103,6 @@ class DeploymentReconciler(KubernetesObjectReconciler[V1Deployment]):
image_pull_secrets=[ image_pull_secrets=[
V1ObjectReference(name=secret) for secret in image_pull_secrets V1ObjectReference(name=secret) for secret in image_pull_secrets
], ],
security_context=V1PodSecurityContext(
seccomp_profile=V1SeccompProfile(
type="RuntimeDefault",
),
),
containers=[ containers=[
V1Container( V1Container(
name=str(self.outpost.type), name=str(self.outpost.type),
@ -155,13 +146,6 @@ class DeploymentReconciler(KubernetesObjectReconciler[V1Deployment]):
), ),
), ),
], ],
security_context=V1SecurityContext(
run_as_non_root=True,
allow_privilege_escalation=False,
capabilities=V1Capabilities(
drop=["ALL"],
),
),
) )
], ],
), ),

View File

@ -1,20 +0,0 @@
# Generated by Django 4.1.7 on 2023-03-20 10:58
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_outposts", "0019_alter_outpost_name_and_more"),
]
operations = [
migrations.AlterField(
model_name="outpost",
name="type",
field=models.TextField(
choices=[("proxy", "Proxy"), ("ldap", "Ldap"), ("radius", "Radius")],
default="proxy",
),
),
]

View File

@ -94,7 +94,6 @@ class OutpostType(models.TextChoices):
PROXY = "proxy" PROXY = "proxy"
LDAP = "ldap" LDAP = "ldap"
RADIUS = "radius"
def default_outpost_config(host: Optional[str] = None): def default_outpost_config(host: Optional[str] = None):

View File

@ -7,7 +7,6 @@ from urllib.parse import urlparse
import yaml import yaml
from asgiref.sync import async_to_sync from asgiref.sync import async_to_sync
from channels.layers import get_channel_layer
from django.core.cache import cache from django.core.cache import cache
from django.db import DatabaseError, InternalError, ProgrammingError from django.db import DatabaseError, InternalError, ProgrammingError
from django.db.models.base import Model from django.db.models.base import Model
@ -43,6 +42,7 @@ from authentik.providers.ldap.controllers.kubernetes import LDAPKubernetesContro
from authentik.providers.proxy.controllers.docker import ProxyDockerController from authentik.providers.proxy.controllers.docker import ProxyDockerController
from authentik.providers.proxy.controllers.kubernetes import ProxyKubernetesController from authentik.providers.proxy.controllers.kubernetes import ProxyKubernetesController
from authentik.root.celery import CELERY_APP from authentik.root.celery import CELERY_APP
from authentik.root.messages.storage import closing_send
LOGGER = get_logger() LOGGER = get_logger()
CACHE_KEY_OUTPOST_DOWN = "outpost_teardown_%s" CACHE_KEY_OUTPOST_DOWN = "outpost_teardown_%s"
@ -214,29 +214,26 @@ def outpost_post_save(model_class: str, model_pk: Any):
outpost_send_update(reverse) outpost_send_update(reverse)
def outpost_send_update(model_instance: Model): def outpost_send_update(model_instace: Model):
"""Send outpost update to all registered outposts, regardless to which authentik """Send outpost update to all registered outposts, regardless to which authentik
instance they are connected""" instance they are connected"""
channel_layer = get_channel_layer() if isinstance(model_instace, OutpostModel):
if isinstance(model_instance, OutpostModel): for outpost in model_instace.outpost_set.all():
for outpost in model_instance.outpost_set.all(): _outpost_single_update(outpost)
_outpost_single_update(outpost, channel_layer) elif isinstance(model_instace, Outpost):
elif isinstance(model_instance, Outpost): _outpost_single_update(model_instace)
_outpost_single_update(model_instance, channel_layer)
def _outpost_single_update(outpost: Outpost, layer=None): def _outpost_single_update(outpost: Outpost):
"""Update outpost instances connected to a single outpost""" """Update outpost instances connected to a single outpost"""
# Ensure token again, because this function is called when anything related to an # Ensure token again, because this function is called when anything related to an
# OutpostModel is saved, so we can be sure permissions are right # OutpostModel is saved, so we can be sure permissions are right
_ = outpost.token _ = outpost.token
outpost.build_user_permissions(outpost.user) outpost.build_user_permissions(outpost.user)
if not layer: # pragma: no cover
layer = get_channel_layer()
for state in OutpostState.for_outpost(outpost): for state in OutpostState.for_outpost(outpost):
for channel in state.channel_ids: for channel in state.channel_ids:
LOGGER.debug("sending update", channel=channel, instance=state.uid, outpost=outpost) LOGGER.debug("sending update", channel=channel, instance=state.uid, outpost=outpost)
async_to_sync(layer.send)(channel, {"type": "event.update"}) async_to_sync(closing_send)(channel, {"type": "event.update"})
@CELERY_APP.task( @CELERY_APP.task(

View File

@ -1,8 +0,0 @@
"""Outpost Websocket URLS"""
from django.urls import path
from authentik.outposts.channels import OutpostConsumer
websocket_urlpatterns = [
path("ws/outpost/<uuid:pk>/", OutpostConsumer.as_asgi()),
]

View File

@ -9,6 +9,8 @@ from authentik.flows.planner import PLAN_CONTEXT_SSO
from authentik.lib.expression.evaluator import BaseEvaluator from authentik.lib.expression.evaluator import BaseEvaluator
from authentik.lib.utils.http import get_client_ip from authentik.lib.utils.http import get_client_ip
from authentik.policies.exceptions import PolicyException from authentik.policies.exceptions import PolicyException
from authentik.policies.models import Policy, PolicyBinding
from authentik.policies.process import PolicyProcess
from authentik.policies.types import PolicyRequest, PolicyResult from authentik.policies.types import PolicyRequest, PolicyResult
LOGGER = get_logger() LOGGER = get_logger()
@ -30,11 +32,22 @@ class PolicyEvaluator(BaseEvaluator):
# update website/docs/expressions/_functions.md # update website/docs/expressions/_functions.md
self._context["ak_message"] = self.expr_func_message self._context["ak_message"] = self.expr_func_message
self._context["ak_user_has_authenticator"] = self.expr_func_user_has_authenticator self._context["ak_user_has_authenticator"] = self.expr_func_user_has_authenticator
self._context["ak_call_policy"] = self.expr_func_call_policy
def expr_func_message(self, message: str): def expr_func_message(self, message: str):
"""Wrapper to append to messages list, which is returned with PolicyResult""" """Wrapper to append to messages list, which is returned with PolicyResult"""
self._messages.append(message) self._messages.append(message)
def expr_func_call_policy(self, name: str, **kwargs) -> PolicyResult:
"""Call policy by name, with current request"""
policy = Policy.objects.filter(name=name).select_subclasses().first()
if not policy:
raise ValueError(f"Policy '{name}' not found.")
req: PolicyRequest = self._context["request"]
req.context.update(kwargs)
proc = PolicyProcess(PolicyBinding(policy=policy), request=req, connection=None)
return proc.profiling_wrapper()
def set_policy_request(self, request: PolicyRequest): def set_policy_request(self, request: PolicyRequest):
"""Update context based on policy request (if http request is given, update that too)""" """Update context based on policy request (if http request is given, update that too)"""
# update website/docs/expressions/_objects.md # update website/docs/expressions/_objects.md
@ -70,7 +83,6 @@ class PolicyEvaluator(BaseEvaluator):
return PolicyResult(False, str(exc)) return PolicyResult(False, str(exc))
else: else:
policy_result = PolicyResult(False, *self._messages) policy_result = PolicyResult(False, *self._messages)
policy_result.raw_result = result
if result is None: if result is None:
LOGGER.warning( LOGGER.warning(
"Expression policy returned None", "Expression policy returned None",

View File

@ -54,7 +54,6 @@ class TestPasswordPolicyFlow(FlowTestCase):
component="ak-stage-prompt", component="ak-stage-prompt",
fields=[ fields=[
{ {
"choices": None,
"field_key": "password", "field_key": "password",
"label": "PASSWORD_LABEL", "label": "PASSWORD_LABEL",
"order": 0, "order": 0,

View File

@ -132,9 +132,9 @@ class TestPolicyProcess(TestCase):
) )
binding = PolicyBinding(policy=policy, target=Application.objects.create(name="test")) binding = PolicyBinding(policy=policy, target=Application.objects.create(name="test"))
http_request = self.factory.get(reverse("authentik_api:user-impersonate-end")) http_request = self.factory.get(reverse("authentik_core:impersonate-end"))
http_request.user = self.user http_request.user = self.user
http_request.resolver_match = resolve(reverse("authentik_api:user-impersonate-end")) http_request.resolver_match = resolve(reverse("authentik_core:impersonate-end"))
request = PolicyRequest(self.user) request = PolicyRequest(self.user)
request.set_http_request(http_request) request.set_http_request(http_request)

View File

@ -69,11 +69,10 @@ class PolicyRequest:
@dataclass @dataclass
class PolicyResult: class PolicyResult:
"""Result from evaluating a policy.""" """Small data-class to hold policy results"""
passing: bool passing: bool
messages: tuple[str, ...] messages: tuple[str, ...]
raw_result: Any
source_binding: Optional["PolicyBinding"] source_binding: Optional["PolicyBinding"]
source_results: Optional[list["PolicyResult"]] source_results: Optional[list["PolicyResult"]]
@ -84,7 +83,6 @@ class PolicyResult:
super().__init__() super().__init__()
self.passing = passing self.passing = passing
self.messages = messages self.messages = messages
self.raw_result = None
self.source_binding = None self.source_binding = None
self.source_results = [] self.source_results = []
self.log_messages = [] self.log_messages = []

View File

@ -3,8 +3,6 @@
import django.utils.timezone import django.utils.timezone
from django.db import migrations, models from django.db import migrations, models
import authentik.providers.oauth2.models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
@ -39,14 +37,4 @@ class Migration(migrations.Migration):
), ),
preserve_default=False, preserve_default=False,
), ),
migrations.AlterField(
model_name="oauth2provider",
name="client_secret",
field=models.CharField(
blank=True,
default=authentik.providers.oauth2.models.generate_client_secret,
max_length=255,
verbose_name="Client Secret",
),
),
] ]

View File

@ -27,11 +27,6 @@ from authentik.providers.oauth2.id_token import IDToken, SubModes
from authentik.sources.oauth.models import OAuthSource from authentik.sources.oauth.models import OAuthSource
def generate_client_secret() -> str:
"""Generate client secret with adequate length"""
return generate_id(128)
class ClientTypes(models.TextChoices): class ClientTypes(models.TextChoices):
"""Confidential clients are capable of maintaining the confidentiality """Confidential clients are capable of maintaining the confidentiality
of their credentials. Public clients are incapable.""" of their credentials. Public clients are incapable."""
@ -137,7 +132,7 @@ class OAuth2Provider(Provider):
max_length=255, max_length=255,
blank=True, blank=True,
verbose_name=_("Client Secret"), verbose_name=_("Client Secret"),
default=generate_client_secret, default=generate_key,
) )
redirect_uris = models.TextField( redirect_uris = models.TextField(
default="", default="",

View File

@ -7,6 +7,7 @@ from rest_framework.test import APITestCase
from authentik.blueprints.tests import apply_blueprint from authentik.blueprints.tests import apply_blueprint
from authentik.core.models import Application from authentik.core.models import Application
from authentik.core.tests.utils import create_test_admin_user, create_test_flow from authentik.core.tests.utils import create_test_admin_user, create_test_flow
from authentik.lib.generators import generate_id, generate_key
from authentik.providers.oauth2.models import OAuth2Provider, ScopeMapping from authentik.providers.oauth2.models import OAuth2Provider, ScopeMapping
@ -17,6 +18,8 @@ class TestAPI(APITestCase):
def setUp(self) -> None: def setUp(self) -> None:
self.provider: OAuth2Provider = OAuth2Provider.objects.create( self.provider: OAuth2Provider = OAuth2Provider.objects.create(
name="test", name="test",
client_id=generate_id(),
client_secret=generate_key(),
authorization_flow=create_test_flow(), authorization_flow=create_test_flow(),
redirect_uris="http://testserver", redirect_uris="http://testserver",
) )

View File

@ -9,7 +9,7 @@ from authentik.core.models import Application
from authentik.core.tests.utils import create_test_admin_user, create_test_flow from authentik.core.tests.utils import create_test_admin_user, create_test_flow
from authentik.events.models import Event, EventAction from authentik.events.models import Event, EventAction
from authentik.flows.challenge import ChallengeTypes from authentik.flows.challenge import ChallengeTypes
from authentik.lib.generators import generate_id from authentik.lib.generators import generate_id, generate_key
from authentik.lib.utils.time import timedelta_from_string from authentik.lib.utils.time import timedelta_from_string
from authentik.providers.oauth2.constants import TOKEN_TYPE from authentik.providers.oauth2.constants import TOKEN_TYPE
from authentik.providers.oauth2.errors import AuthorizeError, ClientIdError, RedirectUriError from authentik.providers.oauth2.errors import AuthorizeError, ClientIdError, RedirectUriError
@ -298,6 +298,7 @@ class TestAuthorize(OAuthTestCase):
provider: OAuth2Provider = OAuth2Provider.objects.create( provider: OAuth2Provider = OAuth2Provider.objects.create(
name=generate_id(), name=generate_id(),
client_id="test", client_id="test",
client_secret=generate_key(),
authorization_flow=flow, authorization_flow=flow,
redirect_uris="http://localhost", redirect_uris="http://localhost",
signing_key=self.keypair, signing_key=self.keypair,
@ -360,6 +361,7 @@ class TestAuthorize(OAuthTestCase):
provider: OAuth2Provider = OAuth2Provider.objects.create( provider: OAuth2Provider = OAuth2Provider.objects.create(
name=generate_id(), name=generate_id(),
client_id="test", client_id="test",
client_secret=generate_key(),
authorization_flow=flow, authorization_flow=flow,
redirect_uris="http://localhost", redirect_uris="http://localhost",
signing_key=self.keypair, signing_key=self.keypair,
@ -415,6 +417,7 @@ class TestAuthorize(OAuthTestCase):
provider = OAuth2Provider.objects.create( provider = OAuth2Provider.objects.create(
name=generate_id(), name=generate_id(),
client_id=generate_id(), client_id=generate_id(),
client_secret=generate_key(),
authorization_flow=flow, authorization_flow=flow,
redirect_uris="http://localhost", redirect_uris="http://localhost",
signing_key=self.keypair, signing_key=self.keypair,
@ -464,6 +467,7 @@ class TestAuthorize(OAuthTestCase):
provider = OAuth2Provider.objects.create( provider = OAuth2Provider.objects.create(
name=generate_id(), name=generate_id(),
client_id=generate_id(), client_id=generate_id(),
client_secret=generate_key(),
authorization_flow=flow, authorization_flow=flow,
redirect_uris="http://localhost", redirect_uris="http://localhost",
signing_key=self.keypair, signing_key=self.keypair,

View File

@ -8,7 +8,7 @@ from django.utils import timezone
from authentik.core.models import Application 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, generate_key
from authentik.providers.oauth2.constants import ACR_AUTHENTIK_DEFAULT from authentik.providers.oauth2.constants import ACR_AUTHENTIK_DEFAULT
from authentik.providers.oauth2.models import AccessToken, IDToken, OAuth2Provider, RefreshToken from authentik.providers.oauth2.models import AccessToken, IDToken, OAuth2Provider, RefreshToken
from authentik.providers.oauth2.tests.utils import OAuthTestCase from authentik.providers.oauth2.tests.utils import OAuthTestCase
@ -21,6 +21,8 @@ class TesOAuth2Introspection(OAuthTestCase):
super().setUp() super().setUp()
self.provider: OAuth2Provider = OAuth2Provider.objects.create( self.provider: OAuth2Provider = OAuth2Provider.objects.create(
name=generate_id(), name=generate_id(),
client_id=generate_id(),
client_secret=generate_key(),
authorization_flow=create_test_flow(), authorization_flow=create_test_flow(),
redirect_uris="", redirect_uris="",
signing_key=create_test_cert(), signing_key=create_test_cert(),

View File

@ -8,7 +8,7 @@ from django.utils import timezone
from authentik.core.models import Application 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, generate_key
from authentik.providers.oauth2.models import AccessToken, IDToken, OAuth2Provider, RefreshToken from authentik.providers.oauth2.models import AccessToken, IDToken, OAuth2Provider, RefreshToken
from authentik.providers.oauth2.tests.utils import OAuthTestCase from authentik.providers.oauth2.tests.utils import OAuthTestCase
@ -20,6 +20,8 @@ class TesOAuth2Revoke(OAuthTestCase):
super().setUp() super().setUp()
self.provider: OAuth2Provider = OAuth2Provider.objects.create( self.provider: OAuth2Provider = OAuth2Provider.objects.create(
name=generate_id(), name=generate_id(),
client_id=generate_id(),
client_secret=generate_key(),
authorization_flow=create_test_flow(), authorization_flow=create_test_flow(),
redirect_uris="", redirect_uris="",
signing_key=create_test_cert(), signing_key=create_test_cert(),

View File

@ -38,6 +38,8 @@ class TestToken(OAuthTestCase):
"""test request param""" """test request param"""
provider = OAuth2Provider.objects.create( provider = OAuth2Provider.objects.create(
name=generate_id(), name=generate_id(),
client_id=generate_id(),
client_secret=generate_key(),
authorization_flow=create_test_flow(), authorization_flow=create_test_flow(),
redirect_uris="http://TestServer", redirect_uris="http://TestServer",
signing_key=self.keypair, signing_key=self.keypair,
@ -65,6 +67,8 @@ class TestToken(OAuthTestCase):
"""test request param""" """test request param"""
provider = OAuth2Provider.objects.create( provider = OAuth2Provider.objects.create(
name=generate_id(), name=generate_id(),
client_id=generate_id(),
client_secret=generate_key(),
authorization_flow=create_test_flow(), authorization_flow=create_test_flow(),
redirect_uris="http://testserver", redirect_uris="http://testserver",
signing_key=self.keypair, signing_key=self.keypair,
@ -86,6 +90,8 @@ class TestToken(OAuthTestCase):
"""test request param""" """test request param"""
provider = OAuth2Provider.objects.create( provider = OAuth2Provider.objects.create(
name=generate_id(), name=generate_id(),
client_id=generate_id(),
client_secret=generate_key(),
authorization_flow=create_test_flow(), authorization_flow=create_test_flow(),
redirect_uris="http://local.invalid", redirect_uris="http://local.invalid",
signing_key=self.keypair, signing_key=self.keypair,
@ -114,6 +120,8 @@ class TestToken(OAuthTestCase):
"""test request param""" """test request param"""
provider = OAuth2Provider.objects.create( provider = OAuth2Provider.objects.create(
name=generate_id(), name=generate_id(),
client_id=generate_id(),
client_secret=generate_key(),
authorization_flow=create_test_flow(), authorization_flow=create_test_flow(),
redirect_uris="http://local.invalid", redirect_uris="http://local.invalid",
signing_key=self.keypair, signing_key=self.keypair,
@ -155,6 +163,8 @@ class TestToken(OAuthTestCase):
"""test request param""" """test request param"""
provider = OAuth2Provider.objects.create( provider = OAuth2Provider.objects.create(
name=generate_id(), name=generate_id(),
client_id=generate_id(),
client_secret=generate_key(),
authorization_flow=create_test_flow(), authorization_flow=create_test_flow(),
redirect_uris="http://local.invalid", redirect_uris="http://local.invalid",
signing_key=self.keypair, signing_key=self.keypair,
@ -205,6 +215,8 @@ class TestToken(OAuthTestCase):
"""test request param""" """test request param"""
provider = OAuth2Provider.objects.create( provider = OAuth2Provider.objects.create(
name=generate_id(), name=generate_id(),
client_id=generate_id(),
client_secret=generate_key(),
authorization_flow=create_test_flow(), authorization_flow=create_test_flow(),
redirect_uris="http://local.invalid", redirect_uris="http://local.invalid",
signing_key=self.keypair, signing_key=self.keypair,
@ -251,6 +263,8 @@ class TestToken(OAuthTestCase):
"""test request param""" """test request param"""
provider = OAuth2Provider.objects.create( provider = OAuth2Provider.objects.create(
name=generate_id(), name=generate_id(),
client_id=generate_id(),
client_secret=generate_key(),
authorization_flow=create_test_flow(), authorization_flow=create_test_flow(),
redirect_uris="http://testserver", redirect_uris="http://testserver",
signing_key=self.keypair, signing_key=self.keypair,

View File

@ -8,6 +8,7 @@ from jwt import decode
from authentik.blueprints.tests import apply_blueprint from authentik.blueprints.tests import apply_blueprint
from authentik.core.models import USER_ATTRIBUTE_SA, Application, Group, Token, TokenIntents from authentik.core.models import USER_ATTRIBUTE_SA, Application, Group, Token, TokenIntents
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, generate_key
from authentik.policies.models import PolicyBinding from authentik.policies.models import PolicyBinding
from authentik.providers.oauth2.constants import ( from authentik.providers.oauth2.constants import (
GRANT_TYPE_CLIENT_CREDENTIALS, GRANT_TYPE_CLIENT_CREDENTIALS,
@ -30,6 +31,8 @@ class TestTokenClientCredentials(OAuthTestCase):
self.factory = RequestFactory() self.factory = RequestFactory()
self.provider = OAuth2Provider.objects.create( self.provider = OAuth2Provider.objects.create(
name="test", name="test",
client_id=generate_id(),
client_secret=generate_key(),
authorization_flow=create_test_flow(), authorization_flow=create_test_flow(),
redirect_uris="http://testserver", redirect_uris="http://testserver",
signing_key=create_test_cert(), signing_key=create_test_cert(),

View File

@ -9,7 +9,7 @@ from jwt import decode
from authentik.blueprints.tests import apply_blueprint from authentik.blueprints.tests import apply_blueprint
from authentik.core.models import Application, Group from authentik.core.models import Application, Group
from authentik.core.tests.utils import create_test_cert, create_test_flow from authentik.core.tests.utils import create_test_cert, create_test_flow
from authentik.lib.generators import generate_id from authentik.lib.generators import generate_id, generate_key
from authentik.policies.models import PolicyBinding from authentik.policies.models import PolicyBinding
from authentik.providers.oauth2.constants import ( from authentik.providers.oauth2.constants import (
GRANT_TYPE_CLIENT_CREDENTIALS, GRANT_TYPE_CLIENT_CREDENTIALS,
@ -39,7 +39,7 @@ class TestTokenClientCredentialsJWTSource(OAuthTestCase):
slug=generate_id(), slug=generate_id(),
provider_type="openidconnect", provider_type="openidconnect",
consumer_key=generate_id(), consumer_key=generate_id(),
consumer_secret=generate_id(), consumer_secret=generate_key(),
authorization_url="http://foo", authorization_url="http://foo",
access_token_url=f"http://{generate_id()}", access_token_url=f"http://{generate_id()}",
profile_url="http://foo", profile_url="http://foo",
@ -52,6 +52,8 @@ class TestTokenClientCredentialsJWTSource(OAuthTestCase):
self.provider: OAuth2Provider = OAuth2Provider.objects.create( self.provider: OAuth2Provider = OAuth2Provider.objects.create(
name="test", name="test",
client_id=generate_id(),
client_secret=generate_key(),
authorization_flow=create_test_flow(), authorization_flow=create_test_flow(),
redirect_uris="http://testserver", redirect_uris="http://testserver",
signing_key=self.cert, signing_key=self.cert,

View File

@ -7,7 +7,7 @@ from django.urls import reverse
from authentik.blueprints.tests import apply_blueprint from authentik.blueprints.tests import apply_blueprint
from authentik.core.models import Application 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_code_fixed_length, generate_id from authentik.lib.generators import generate_code_fixed_length, generate_id, generate_key
from authentik.providers.oauth2.constants import GRANT_TYPE_DEVICE_CODE from authentik.providers.oauth2.constants import GRANT_TYPE_DEVICE_CODE
from authentik.providers.oauth2.models import DeviceToken, OAuth2Provider, ScopeMapping from authentik.providers.oauth2.models import DeviceToken, OAuth2Provider, ScopeMapping
from authentik.providers.oauth2.tests.utils import OAuthTestCase from authentik.providers.oauth2.tests.utils import OAuthTestCase
@ -22,6 +22,8 @@ class TestTokenDeviceCode(OAuthTestCase):
self.factory = RequestFactory() self.factory = RequestFactory()
self.provider = OAuth2Provider.objects.create( self.provider = OAuth2Provider.objects.create(
name="test", name="test",
client_id=generate_id(),
client_secret=generate_key(),
authorization_flow=create_test_flow(), authorization_flow=create_test_flow(),
redirect_uris="http://testserver", redirect_uris="http://testserver",
signing_key=create_test_cert(), signing_key=create_test_cert(),

View File

@ -9,7 +9,7 @@ from authentik.blueprints.tests import apply_blueprint
from authentik.core.models import Application 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.events.models import Event, EventAction from authentik.events.models import Event, EventAction
from authentik.lib.generators import generate_id from authentik.lib.generators import generate_id, generate_key
from authentik.providers.oauth2.models import AccessToken, IDToken, OAuth2Provider, ScopeMapping from authentik.providers.oauth2.models import AccessToken, IDToken, OAuth2Provider, ScopeMapping
from authentik.providers.oauth2.tests.utils import OAuthTestCase from authentik.providers.oauth2.tests.utils import OAuthTestCase
@ -23,6 +23,8 @@ class TestUserinfo(OAuthTestCase):
self.app = Application.objects.create(name=generate_id(), slug=generate_id()) self.app = Application.objects.create(name=generate_id(), slug=generate_id())
self.provider: OAuth2Provider = OAuth2Provider.objects.create( self.provider: OAuth2Provider = OAuth2Provider.objects.create(
name=generate_id(), name=generate_id(),
client_id=generate_id(),
client_secret=generate_key(),
authorization_flow=create_test_flow(), authorization_flow=create_test_flow(),
redirect_uris="", redirect_uris="",
signing_key=create_test_cert(), signing_key=create_test_cert(),

View File

@ -1,65 +0,0 @@
"""RadiusProvider API Views"""
from rest_framework.fields import CharField
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
from authentik.core.api.providers import ProviderSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.providers.radius.models import RadiusProvider
class RadiusProviderSerializer(ProviderSerializer):
"""RadiusProvider Serializer"""
class Meta:
model = RadiusProvider
fields = ProviderSerializer.Meta.fields + [
"client_networks",
# Shared secret is not a write-only field, as
# an admin might have to view it
"shared_secret",
]
extra_kwargs = ProviderSerializer.Meta.extra_kwargs
class RadiusProviderViewSet(UsedByMixin, ModelViewSet):
"""RadiusProvider Viewset"""
queryset = RadiusProvider.objects.all()
serializer_class = RadiusProviderSerializer
ordering = ["name"]
search_fields = ["name", "client_networks"]
filterset_fields = {
"application": ["isnull"],
"name": ["iexact"],
"authorization_flow__slug": ["iexact"],
"client_networks": ["iexact"],
}
class RadiusOutpostConfigSerializer(ModelSerializer):
"""RadiusProvider Serializer"""
application_slug = CharField(source="application.slug")
auth_flow_slug = CharField(source="authorization_flow.slug")
class Meta:
model = RadiusProvider
fields = [
"pk",
"name",
"application_slug",
"auth_flow_slug",
"client_networks",
"shared_secret",
]
class RadiusOutpostConfigViewSet(ReadOnlyModelViewSet):
"""RadiusProvider Viewset"""
queryset = RadiusProvider.objects.filter(application__isnull=False)
serializer_class = RadiusOutpostConfigSerializer
ordering = ["name"]
search_fields = ["name"]
filterset_fields = ["name"]

View File

@ -1,10 +0,0 @@
"""authentik radius provider app config"""
from django.apps import AppConfig
class AuthentikProviderRadiusConfig(AppConfig):
"""authentik radius provider app config"""
name = "authentik.providers.radius"
label = "authentik_providers_radius"
verbose_name = "authentik Providers.Radius"

View File

@ -1,14 +0,0 @@
"""Radius Provider Docker Controller"""
from authentik.outposts.controllers.base import DeploymentPort
from authentik.outposts.controllers.docker import DockerController
from authentik.outposts.models import DockerServiceConnection, Outpost
class RadiusDockerController(DockerController):
"""Radius Provider Docker Controller"""
def __init__(self, outpost: Outpost, connection: DockerServiceConnection):
super().__init__(outpost, connection)
self.deployment_ports = [
DeploymentPort(1812, "radius", "udp", 1812),
]

View File

@ -1,14 +0,0 @@
"""Radius Provider Kubernetes Controller"""
from authentik.outposts.controllers.base import DeploymentPort
from authentik.outposts.controllers.kubernetes import KubernetesController
from authentik.outposts.models import KubernetesServiceConnection, Outpost
class RadiusKubernetesController(KubernetesController):
"""Radius Provider Kubernetes Controller"""
def __init__(self, outpost: Outpost, connection: KubernetesServiceConnection):
super().__init__(outpost, connection)
self.deployment_ports = [
DeploymentPort(1812, "radius", "udp", 1812),
]

View File

@ -1,52 +0,0 @@
# Generated by Django 4.1.7 on 2023-03-20 10:58
import django.db.models.deletion
from django.db import migrations, models
import authentik.lib.generators
class Migration(migrations.Migration):
initial = True
dependencies = [
("authentik_core", "0027_alter_user_uuid"),
]
operations = [
migrations.CreateModel(
name="RadiusProvider",
fields=[
(
"provider_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="authentik_core.provider",
),
),
(
"shared_secret",
models.TextField(
default=authentik.lib.generators.generate_id,
help_text="Shared secret between clients and server to hash packets.",
),
),
(
"client_networks",
models.TextField(
default="0.0.0.0/0, ::/0",
help_text="List of CIDRs (comma-separated) that clients can connect from. A more specific CIDR will match before a looser one. Clients connecting from a non-specified CIDR will be dropped.",
),
),
],
options={
"verbose_name": "Radius Provider",
"verbose_name_plural": "Radius Providers",
},
bases=("authentik_core.provider", models.Model),
),
]

View File

@ -1,50 +0,0 @@
"""Radius Provider"""
from typing import Optional, Type
from django.db import models
from django.utils.translation import gettext_lazy as _
from rest_framework.serializers import Serializer
from authentik.core.models import Provider
from authentik.lib.generators import generate_id
from authentik.outposts.models import OutpostModel
class RadiusProvider(OutpostModel, Provider):
"""Allow applications to authenticate against authentik's users using Radius."""
shared_secret = models.TextField(
default=generate_id,
help_text=_("Shared secret between clients and server to hash packets."),
)
client_networks = models.TextField(
default="0.0.0.0/0, ::/0",
help_text=_(
"List of CIDRs (comma-separated) that clients can connect from. A more specific "
"CIDR will match before a looser one. Clients connecting from a non-specified CIDR "
"will be dropped."
),
)
@property
def launch_url(self) -> Optional[str]:
"""Radius never has a launch URL"""
return None
@property
def component(self) -> str:
return "ak-provider-radius-form"
@property
def serializer(self) -> Type[Serializer]:
from authentik.providers.radius.api import RadiusProviderSerializer
return RadiusProviderSerializer
def __str__(self):
return f"Radius Provider {self.name}"
class Meta:
verbose_name = _("Radius Provider")
verbose_name_plural = _("Radius Providers")

View File

@ -34,16 +34,8 @@ def pre_delete_scim(sender: type[Model], instance: User | Group, **_):
@receiver(m2m_changed, sender=User.ak_groups.through) @receiver(m2m_changed, sender=User.ak_groups.through)
def m2m_changed_scim( def m2m_changed_scim(sender: type[Model], instance, action: str, pk_set: set, **kwargs):
sender: type[Model], instance, action: str, pk_set: set, reverse: bool, **kwargs
):
"""Sync group membership""" """Sync group membership"""
if action not in ["post_add", "post_remove"]: if action not in ["post_add", "post_remove"]:
return return
# reverse: instance is a Group, pk_set is a list of user pks
# non-reverse: instance is a User, pk_set is a list of groups
if reverse:
scim_signal_m2m.delay(str(instance.pk), action, list(pk_set)) scim_signal_m2m.delay(str(instance.pk), action, list(pk_set))
else:
for group_pk in pk_set:
scim_signal_m2m.delay(group_pk, action, [instance.pk])

View File

@ -151,7 +151,7 @@ def scim_signal_direct(model: str, pk: Any, raw_op: str):
@CELERY_APP.task() @CELERY_APP.task()
def scim_signal_m2m(group_pk: str, action: str, pk_set: list[int]): def scim_signal_m2m(group_pk: str, action: str, pk_set: set[int]):
"""Update m2m (group membership)""" """Update m2m (group membership)"""
group = Group.objects.filter(pk=group_pk).first() group = Group.objects.filter(pk=group_pk).first()
if not group: if not group:

View File

@ -82,11 +82,9 @@ class SCIMMembershipTests(TestCase):
mocker.request_history[3].body, mocker.request_history[3].body,
{ {
"emails": [], "emails": [],
"active": True,
"externalId": user.uid, "externalId": user.uid,
"name": {"familyName": "", "formatted": "", "givenName": ""}, "name": {"familyName": "", "formatted": "", "givenName": ""},
"photos": [], "photos": [],
"displayName": "",
"userName": user.username, "userName": user.username,
}, },
) )
@ -165,8 +163,6 @@ class SCIMMembershipTests(TestCase):
self.assertJSONEqual( self.assertJSONEqual(
mocker.request_history[3].body, mocker.request_history[3].body,
{ {
"active": True,
"displayName": "",
"emails": [], "emails": [],
"externalId": user.uid, "externalId": user.uid,
"name": {"familyName": "", "formatted": "", "givenName": ""}, "name": {"familyName": "", "formatted": "", "givenName": ""},

View File

@ -61,7 +61,6 @@ class SCIMUserTests(TestCase):
self.assertJSONEqual( self.assertJSONEqual(
mock.request_history[1].body, mock.request_history[1].body,
{ {
"active": True,
"emails": [ "emails": [
{ {
"primary": True, "primary": True,
@ -75,7 +74,6 @@ class SCIMUserTests(TestCase):
"formatted": uid, "formatted": uid,
"givenName": uid, "givenName": uid,
}, },
"displayName": uid,
"photos": [], "photos": [],
"userName": uid, "userName": uid,
}, },
@ -117,7 +115,6 @@ class SCIMUserTests(TestCase):
self.assertEqual( self.assertEqual(
body, body,
{ {
"active": True,
"emails": [ "emails": [
{ {
"primary": True, "primary": True,
@ -125,7 +122,6 @@ class SCIMUserTests(TestCase):
"value": f"{uid}@goauthentik.io", "value": f"{uid}@goauthentik.io",
} }
], ],
"displayName": uid,
"externalId": user.uid, "externalId": user.uid,
"name": { "name": {
"familyName": "", "familyName": "",
@ -170,7 +166,6 @@ class SCIMUserTests(TestCase):
self.assertJSONEqual( self.assertJSONEqual(
mock.request_history[1].body, mock.request_history[1].body,
{ {
"active": True,
"emails": [ "emails": [
{ {
"primary": True, "primary": True,
@ -184,7 +179,6 @@ class SCIMUserTests(TestCase):
"formatted": uid, "formatted": uid,
"givenName": uid, "givenName": uid,
}, },
"displayName": uid,
"photos": [], "photos": [],
"userName": uid, "userName": uid,
}, },
@ -238,7 +232,6 @@ class SCIMUserTests(TestCase):
self.assertJSONEqual( self.assertJSONEqual(
mock.request_history[1].body, mock.request_history[1].body,
{ {
"active": True,
"emails": [ "emails": [
{ {
"primary": True, "primary": True,
@ -252,7 +245,6 @@ class SCIMUserTests(TestCase):
"formatted": uid, "formatted": uid,
"givenName": uid, "givenName": uid,
}, },
"displayName": uid,
"photos": [], "photos": [],
"userName": uid, "userName": uid,
}, },

View File

@ -2,12 +2,9 @@
import os import os
from contextvars import ContextVar from contextvars import ContextVar
from logging.config import dictConfig from logging.config import dictConfig
from pathlib import Path
from tempfile import gettempdir
from typing import Callable from typing import Callable
from celery import Celery, bootsteps from celery import Celery
from celery.apps.worker import Worker
from celery.signals import ( from celery.signals import (
after_task_publish, after_task_publish,
setup_logging, setup_logging,
@ -31,7 +28,6 @@ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "authentik.root.settings")
LOGGER = get_logger() LOGGER = get_logger()
CELERY_APP = Celery("authentik") CELERY_APP = Celery("authentik")
CTX_TASK_ID = ContextVar(STRUCTLOG_KEY_PREFIX + "task_id", default=Ellipsis) CTX_TASK_ID = ContextVar(STRUCTLOG_KEY_PREFIX + "task_id", default=Ellipsis)
HEARTBEAT_FILE = Path(gettempdir() + "/authentik-worker")
@setup_logging.connect @setup_logging.connect
@ -103,33 +99,6 @@ def worker_ready_hook(*args, **kwargs):
start_blueprint_watcher() start_blueprint_watcher()
class LivenessProbe(bootsteps.StartStopStep):
"""Add a timed task to touch a temporary file for healthchecking reasons"""
requires = {"celery.worker.components:Timer"}
def __init__(self, parent, **kwargs):
super().__init__(parent, **kwargs)
self.requests = []
self.tref = None
def start(self, parent: Worker):
self.tref = parent.timer.call_repeatedly(
10.0,
self.update_heartbeat_file,
(parent,),
priority=10,
)
self.update_heartbeat_file(parent)
def stop(self, parent: Worker):
HEARTBEAT_FILE.unlink(missing_ok=True)
def update_heartbeat_file(self, worker: Worker):
"""Touch heartbeat file"""
HEARTBEAT_FILE.touch()
# Using a string here means the worker doesn't have to serialize # Using a string here means the worker doesn't have to serialize
# the configuration object to child processes. # the configuration object to child processes.
# - namespace='CELERY' means all celery-related configuration keys # - namespace='CELERY' means all celery-related configuration keys
@ -138,4 +107,3 @@ CELERY_APP.config_from_object(settings, namespace="CELERY")
# Load task modules from all registered Django app configs. # Load task modules from all registered Django app configs.
CELERY_APP.autodiscover_tasks() CELERY_APP.autodiscover_tasks()
CELERY_APP.steps["worker"].add(LivenessProbe)

View File

@ -1,6 +1,7 @@
"""Channels Messages storage""" """Channels Messages storage"""
from asgiref.sync import async_to_sync from asgiref.sync import async_to_sync
from channels.layers import get_channel_layer from channels import DEFAULT_CHANNEL_LAYER
from channels.layers import channel_layers
from django.contrib.messages.storage.base import Message from django.contrib.messages.storage.base import Message
from django.contrib.messages.storage.session import SessionStorage from django.contrib.messages.storage.session import SessionStorage
from django.core.cache import cache from django.core.cache import cache
@ -10,13 +11,21 @@ SESSION_KEY = "_messages"
CACHE_PREFIX = "goauthentik.io/root/messages_" CACHE_PREFIX = "goauthentik.io/root/messages_"
async def closing_send(channel, message):
"""Wrapper around layer send that closes the connection"""
# See https://github.com/django/channels_redis/issues/332
# TODO: Remove this after channels_redis 4.1 is released
channel_layer = channel_layers.make_backend(DEFAULT_CHANNEL_LAYER)
await channel_layer.send(channel, message)
await channel_layer.close_pools()
class ChannelsStorage(SessionStorage): class ChannelsStorage(SessionStorage):
"""Send contrib.messages over websocket""" """Send contrib.messages over websocket"""
def __init__(self, request: HttpRequest) -> None: def __init__(self, request: HttpRequest) -> None:
# pyright: reportGeneralTypeIssues=false # pyright: reportGeneralTypeIssues=false
super().__init__(request) super().__init__(request)
self.channel = get_channel_layer()
def _store(self, messages: list[Message], response, *args, **kwargs): def _store(self, messages: list[Message], response, *args, **kwargs):
prefix = f"{CACHE_PREFIX}{self.request.session.session_key}_messages_" prefix = f"{CACHE_PREFIX}{self.request.session.session_key}_messages_"
@ -28,7 +37,7 @@ class ChannelsStorage(SessionStorage):
for key in keys: for key in keys:
uid = key.replace(prefix, "") uid = key.replace(prefix, "")
for message in messages: for message in messages:
async_to_sync(self.channel.send)( async_to_sync(closing_send)(
uid, uid,
{ {
"type": "event.update", "type": "event.update",

View File

@ -79,7 +79,6 @@ INSTALLED_APPS = [
"authentik.providers.ldap", "authentik.providers.ldap",
"authentik.providers.oauth2", "authentik.providers.oauth2",
"authentik.providers.proxy", "authentik.providers.proxy",
"authentik.providers.radius",
"authentik.providers.saml", "authentik.providers.saml",
"authentik.providers.scim", "authentik.providers.scim",
"authentik.recovery", "authentik.recovery",
@ -276,10 +275,6 @@ DATABASES = {
"USER": CONFIG.y("postgresql.user"), "USER": CONFIG.y("postgresql.user"),
"PASSWORD": CONFIG.y("postgresql.password"), "PASSWORD": CONFIG.y("postgresql.password"),
"PORT": int(CONFIG.y("postgresql.port")), "PORT": int(CONFIG.y("postgresql.port")),
"SSLMODE": CONFIG.y("postgresql.sslmode"),
"SSLROOTCERT": CONFIG.y("postgresql.sslrootcert"),
"SSLCERT": CONFIG.y("postgresql.sslcert"),
"SSLKEY": CONFIG.y("postgresql.sslkey"),
} }
} }

View File

@ -16,7 +16,7 @@ class PytestTestRunner: # pragma: no cover
self.failfast = failfast self.failfast = failfast
self.keepdb = keepdb self.keepdb = keepdb
self.args = ["-vv", "--full-trace"] self.args = ["-vv"]
if self.failfast: if self.failfast:
self.args.append("--exitfirst") self.args.append("--exitfirst")
if self.keepdb: if self.keepdb:

View File

@ -1,21 +1,15 @@
"""root Websocket URLS""" """root Websocket URLS"""
from importlib import import_module from channels.auth import AuthMiddleware
from channels.sessions import CookieMiddleware
from django.urls import path
from structlog.stdlib import get_logger from authentik.outposts.channels import OutpostConsumer
from authentik.root.asgi_middleware import SessionMiddleware
from authentik.root.messages.consumer import MessageConsumer
from authentik.lib.utils.reflection import get_apps websocket_urlpatterns = [
path("ws/outpost/<uuid:pk>/", OutpostConsumer.as_asgi()),
LOGGER = get_logger() path(
"ws/client/", CookieMiddleware(SessionMiddleware(AuthMiddleware(MessageConsumer.as_asgi())))
websocket_urlpatterns = [] ),
for _authentik_app in get_apps(): ]
mountpoint = getattr(_authentik_app, "ws_mountpoint", None)
if not mountpoint:
continue
ws_paths = import_module(mountpoint)
websocket_urlpatterns.extend(getattr(ws_paths, "websocket_urlpatterns"))
LOGGER.debug(
"Mounted URLs",
app_name=_authentik_app.name,
app_mountpoint=mountpoint,
)

View File

@ -2,12 +2,13 @@
from typing import Optional from typing import Optional
from django.http import HttpRequest from django.http import HttpRequest
from ldap3 import Connection
from ldap3.core.exceptions import LDAPException, LDAPInvalidCredentialsResult from ldap3.core.exceptions import LDAPException, LDAPInvalidCredentialsResult
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from authentik.core.auth import InbuiltBackend from authentik.core.auth import InbuiltBackend
from authentik.core.models import User from authentik.core.models import User
from authentik.sources.ldap.models import LDAPSource from authentik.sources.ldap.models import LDAP_TIMEOUT, LDAPSource
LOGGER = get_logger() LOGGER = get_logger()
LDAP_DISTINGUISHED_NAME = "distinguishedName" LDAP_DISTINGUISHED_NAME = "distinguishedName"
@ -57,11 +58,12 @@ class LDAPBackend(InbuiltBackend):
# Try to bind as new user # Try to bind as new user
LOGGER.debug("Attempting Binding as user", user=user) LOGGER.debug("Attempting Binding as user", user=user)
try: try:
temp_connection = source.connection( temp_connection = Connection(
connection_kwargs={ source.server,
"user": user.attributes.get(LDAP_DISTINGUISHED_NAME), user=user.attributes.get(LDAP_DISTINGUISHED_NAME),
"password": password, password=password,
} raise_exceptions=True,
receive_timeout=LDAP_TIMEOUT,
) )
temp_connection.bind() temp_connection.bind()
return user return user

View File

@ -1,11 +1,9 @@
"""authentik LDAP Models""" """authentik LDAP Models"""
from ssl import CERT_REQUIRED from ssl import CERT_REQUIRED
from typing import Optional
from django.db import models from django.db import models
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from ldap3 import ALL, NONE, RANDOM, Connection, Server, ServerPool, Tls from ldap3 import ALL, RANDOM, Connection, Server, ServerPool, Tls
from ldap3.core.exceptions import LDAPSchemaError
from rest_framework.serializers import Serializer from rest_framework.serializers import Serializer
from authentik.core.models import Group, PropertyMapping, Source from authentik.core.models import Group, PropertyMapping, Source
@ -105,7 +103,8 @@ class LDAPSource(Source):
return LDAPSourceSerializer return LDAPSourceSerializer
def server(self, **kwargs) -> Server: @property
def server(self) -> Server:
"""Get LDAP Server/ServerPool""" """Get LDAP Server/ServerPool"""
servers = [] servers = []
tls_kwargs = {} tls_kwargs = {}
@ -114,45 +113,32 @@ class LDAPSource(Source):
tls_kwargs["validate"] = CERT_REQUIRED tls_kwargs["validate"] = CERT_REQUIRED
if ciphers := CONFIG.y("ldap.tls.ciphers", None): if ciphers := CONFIG.y("ldap.tls.ciphers", None):
tls_kwargs["ciphers"] = ciphers.strip() tls_kwargs["ciphers"] = ciphers.strip()
server_kwargs = { kwargs = {
"get_info": ALL, "get_info": ALL,
"connect_timeout": LDAP_TIMEOUT, "connect_timeout": LDAP_TIMEOUT,
"tls": Tls(**tls_kwargs), "tls": Tls(**tls_kwargs),
} }
server_kwargs.update(kwargs)
if "," in self.server_uri: if "," in self.server_uri:
for server in self.server_uri.split(","): for server in self.server_uri.split(","):
servers.append(Server(server, **server_kwargs)) servers.append(Server(server, **kwargs))
else: else:
servers = [Server(self.server_uri, **server_kwargs)] servers = [Server(self.server_uri, **kwargs)]
return ServerPool(servers, RANDOM, active=True, exhaust=True) return ServerPool(servers, RANDOM, active=True, exhaust=True)
def connection( @property
self, server_kwargs: Optional[dict] = None, connection_kwargs: Optional[dict] = None def connection(self) -> Connection:
) -> Connection:
"""Get a fully connected and bound LDAP Connection""" """Get a fully connected and bound LDAP Connection"""
server_kwargs = server_kwargs or {}
connection_kwargs = connection_kwargs or {}
connection_kwargs.setdefault("user", self.bind_cn)
connection_kwargs.setdefault("password", self.bind_password)
connection = Connection( connection = Connection(
self.server(**server_kwargs), self.server,
raise_exceptions=True, raise_exceptions=True,
user=self.bind_cn,
password=self.bind_password,
receive_timeout=LDAP_TIMEOUT, receive_timeout=LDAP_TIMEOUT,
**connection_kwargs,
) )
if self.start_tls: if self.start_tls:
connection.start_tls(read_server_info=False) connection.start_tls(read_server_info=False)
try:
connection.bind() connection.bind()
except LDAPSchemaError as exc:
# Schema error, so try connecting without schema info
# See https://github.com/goauthentik/authentik/issues/4590
if server_kwargs.get("get_info", ALL) == NONE:
raise exc
server_kwargs["get_info"] = NONE
return self.connection(server_kwargs, connection_kwargs)
return connection return connection
class Meta: class Meta:

View File

@ -47,11 +47,10 @@ class LDAPPasswordChanger:
def __init__(self, source: LDAPSource) -> None: def __init__(self, source: LDAPSource) -> None:
self._source = source self._source = source
self._connection = source.connection()
def get_domain_root_dn(self) -> str: def get_domain_root_dn(self) -> str:
"""Attempt to get root DN via MS specific fields or generic LDAP fields""" """Attempt to get root DN via MS specific fields or generic LDAP fields"""
info = self._connection.server.info info = self._source.connection.server.info
if "rootDomainNamingContext" in info.other: if "rootDomainNamingContext" in info.other:
return info.other["rootDomainNamingContext"][0] return info.other["rootDomainNamingContext"][0]
naming_contexts = info.naming_contexts naming_contexts = info.naming_contexts
@ -62,7 +61,7 @@ class LDAPPasswordChanger:
"""Check if DOMAIN_PASSWORD_COMPLEX is enabled""" """Check if DOMAIN_PASSWORD_COMPLEX is enabled"""
root_dn = self.get_domain_root_dn() root_dn = self.get_domain_root_dn()
try: try:
root_attrs = self._connection.extend.standard.paged_search( root_attrs = self._source.connection.extend.standard.paged_search(
search_base=root_dn, search_base=root_dn,
search_filter="(objectClass=*)", search_filter="(objectClass=*)",
search_scope=BASE, search_scope=BASE,
@ -91,14 +90,14 @@ class LDAPPasswordChanger:
LOGGER.info(f"User has no {LDAP_DISTINGUISHED_NAME} set.") LOGGER.info(f"User has no {LDAP_DISTINGUISHED_NAME} set.")
return return
try: try:
self._connection.extend.microsoft.modify_password(user_dn, password) self._source.connection.extend.microsoft.modify_password(user_dn, password)
except LDAPAttributeError: except LDAPAttributeError:
self._connection.extend.standard.modify_password(user_dn, new_password=password) self._source.connection.extend.standard.modify_password(user_dn, new_password=password)
def _ad_check_password_existing(self, password: str, user_dn: str) -> bool: def _ad_check_password_existing(self, password: str, user_dn: str) -> bool:
"""Check if a password contains sAMAccount or displayName""" """Check if a password contains sAMAccount or displayName"""
users = list( users = list(
self._connection.extend.standard.paged_search( self._source.connection.extend.standard.paged_search(
search_base=user_dn, search_base=user_dn,
search_filter=self._source.user_object_filter, search_filter=self._source.user_object_filter,
search_scope=BASE, search_scope=BASE,

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