Compare commits

..

1 Commits

Author SHA1 Message Date
72f85defb8 release: 2023.3.0 2023-03-13 18:30:48 +01:00
426 changed files with 20078 additions and 25938 deletions

View File

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

View File

@ -7,14 +7,8 @@ charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.html]
[html]
indent_size = 2
[*.{yaml,yml}]
[yaml]
indent_size = 2
[*.go]
indent_style = tab
[Makefile]
indent_style = tab

View File

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

View File

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

11
.github/codecov.yml vendored
View File

@ -1,10 +1,3 @@
coverage:
status:
project:
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
precision: 2
round: up

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
recent activity. It will be closed if no further activity occurs. Thank you
for your contributions.
only: issues

View File

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

View File

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

View File

@ -39,32 +39,10 @@ jobs:
- name: test
working-directory: website/
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:
needs:
- lint-prettier
- test
- build
runs-on: ubuntu-latest
steps:
- run: echo mark

View File

@ -10,11 +10,6 @@ jobs:
name: Delete old unused container images
runs-on: ubuntu-latest
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
uses: snok/container-retention-policy@v2
with:
@ -23,5 +18,5 @@ jobs:
account-type: org
org-name: goauthentik
untagged-only: false
token: ${{ steps.generate_token.outputs.token }}
token: ${{ secrets.BOT_GITHUB_TOKEN }}
skip-tags: gh-next,gh-main

View File

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

View File

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

View File

@ -18,23 +18,18 @@ jobs:
compile:
runs-on: ubuntu-latest
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
with:
token: ${{ steps.generate_token.outputs.token }}
token: ${{ secrets.BOT_GITHUB_TOKEN }}
- name: Setup authentik env
uses: ./.github/actions/setup
- name: run compile
run: poetry run ./manage.py compilemessages
- name: Create Pull Request
uses: peter-evans/create-pull-request@v5
uses: peter-evans/create-pull-request@v4
id: cpr
with:
token: ${{ steps.generate_token.outputs.token }}
token: ${{ secrets.BOT_GITHUB_TOKEN }}
branch: compile-backend-translation
commit-message: "core: compile backend translations"
title: "core: compile backend translations"

View File

@ -9,14 +9,9 @@ jobs:
build:
runs-on: ubuntu-latest
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
with:
token: ${{ steps.generate_token.outputs.token }}
token: ${{ secrets.BOT_GITHUB_TOKEN }}
- uses: actions/setup-node@v3.6.0
with:
node-version: '18'
@ -35,10 +30,10 @@ jobs:
run: |
export VERSION=`node -e 'console.log(require("../gen-ts-api/package.json").version)'`
npm i @goauthentik/api@$VERSION
- uses: peter-evans/create-pull-request@v5
- uses: peter-evans/create-pull-request@v4
id: cpr
with:
token: ${{ steps.generate_token.outputs.token }}
token: ${{ secrets.BOT_GITHUB_TOKEN }}
branch: update-web-api-client
commit-message: "web: bump API Client version"
title: "web: bump API Client version"
@ -47,8 +42,8 @@ jobs:
signoff: true
team-reviewers: "@goauthentik/core"
author: authentik bot <github-bot@goauthentik.io>
- uses: peter-evans/enable-pull-request-automerge@v3
- uses: peter-evans/enable-pull-request-automerge@v2
with:
token: ${{ steps.generate_token.outputs.token }}
token: ${{ secrets.BOT_GITHUB_TOKEN }}
pull-request-number: ${{ steps.cpr.outputs.pull-request-number }}
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)
- [Suggesting Enhancements](#suggesting-enhancements)
- [Your First Code Contribution](#your-first-code-contribution)
- [Help with the Docs](#help-with-the-docs)
- [Pull Requests](#pull-requests)
[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)
### 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
The process described here has several goals:

View File

@ -20,7 +20,7 @@ WORKDIR /work/web
RUN npm ci && npm run build
# 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
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
# 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
@ -47,7 +47,7 @@ COPY ./go.sum /work/go.sum
RUN go build -o /work/authentik ./cmd/server/
# 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_VERBOSE="true"
@ -62,7 +62,7 @@ RUN --mount=type=secret,id=GEOIPUPDATE_ACCOUNT_ID \
"
# 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.description goauthentik.io Main server image, see https://goauthentik.io for more info.
@ -102,7 +102,7 @@ COPY ./tests /tests
COPY ./manage.py /
COPY ./blueprints /blueprints
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/authentik/ /web/authentik/
COPY --from=website-builder /work/website/help/ /website/help/

View File

@ -4,20 +4,6 @@ UID = $(shell id -u)
GID = $(shell id -g)
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
test-go:
@ -40,7 +26,14 @@ test:
lint-fix:
isort 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:
pylint authentik tests lifecycle
@ -50,6 +43,9 @@ lint:
migrate:
python -m lifecycle.migrate
run:
go run -v ./cmd/server/
i18n-extract: i18n-extract-core web-extract
i18n-extract-core:
@ -63,20 +59,15 @@ gen-build:
AUTHENTIK_DEBUG=true ak make_blueprint_schema > blueprints/schema.json
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:
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 \
--rm -v ${PWD}:/local \
--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 \
/local/old_schema.yml /local/schema.yml
rm old_schema.yml
npx prettier --write diff.md
gen-clean:
rm -rf web/api/src/
@ -86,7 +77,7 @@ gen-client-ts:
docker run \
--rm -v ${PWD}:/local \
--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 \
-g typescript-fetch \
-o /local/gen-ts-api \
@ -99,21 +90,20 @@ gen-client-ts:
\cp -rfv gen-ts-api/* web/node_modules/@goauthentik/api
gen-client-go:
mkdir -p ./gen-go-api ./gen-go-api/templates
wget https://raw.githubusercontent.com/goauthentik/client-go/main/config.yaml -O ./gen-go-api/config.yaml
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/go.mod.mustache -O ./gen-go-api/templates/go.mod.mustache
cp schema.yml ./gen-go-api/
wget https://raw.githubusercontent.com/goauthentik/client-go/main/config.yaml -O config.yaml
mkdir -p templates
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 templates/go.mod.mustache
docker run \
--rm -v ${PWD}/gen-go-api:/local \
--rm -v ${PWD}:/local \
--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 \
-g go \
-o /local/ \
-o /local/gen-go-api \
-c /local/config.yaml
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:
python -m scripts.generate_config
@ -182,9 +172,6 @@ ci-pylint: ci--meta-debug
ci-black: ci--meta-debug
black --check $(PY_SOURCES)
ci-codespell: ci--meta-debug
codespell $(CODESPELL_ARGS) -s
ci-isort: ci--meta-debug
isort --check $(PY_SOURCES)

View File

@ -15,13 +15,13 @@
## 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
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
@ -32,15 +32,15 @@ For bigger setups, there is a Helm Chart [here](https://github.com/goauthentik/h
## Development
See [Developer Documentation](https://goauthentik.io/developer-docs/?utm_source=github)
See [Development Documentation](https://goauthentik.io/developer-docs/?utm_source=github)
## Security
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

View File

@ -6,8 +6,9 @@ Authentik takes security very seriously. We follow the rules of [responsible dis
| Version | Supported |
| --------- | ------------------ |
| 2022.12.x | :white_check_mark: |
| 2023.1.x | :white_check_mark: |
| 2023.2.x | :white_check_mark: |
| 2023.3.x | :white_check_mark: |
## Reporting a Vulnerability

View File

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

View File

@ -1,4 +1,5 @@
"""authentik administration overview"""
import os
import platform
from datetime import datetime
from sys import version as python_version
@ -33,6 +34,7 @@ class RuntimeDict(TypedDict):
class SystemSerializer(PassiveSerializer):
"""Get system information."""
env = SerializerMethodField()
http_headers = SerializerMethodField()
http_host = SerializerMethodField()
http_is_secure = SerializerMethodField()
@ -41,6 +43,10 @@ class SystemSerializer(PassiveSerializer):
server_time = 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]:
"""Get HTTP Request headers"""
headers = {}

View File

@ -1,5 +1,4 @@
"""API Authentication"""
from hmac import compare_digest
from typing import Any, Optional
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"""
from authentik.outposts.apps import MANAGED_OUTPOST
if not compare_digest(value, settings.SECRET_KEY):
if value != settings.SECRET_KEY:
return None
outposts = Outpost.objects.filter(managed=MANAGED_OUTPOST)
if not outposts:

View File

@ -7,13 +7,82 @@ API Browser - {{ tenant.branding_title }}
{% endblock %}
{% block head %}
<script src="{% static 'dist/standalone/api-browser/index.js' %}?version={{ version }}" type="module"></script>
<meta name="theme-color" content="#151515" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#151515" media="(prefers-color-scheme: dark)">
<link rel="icon" href="{{ tenant.branding_favicon }}">
<link rel="shortcut icon" href="{{ tenant.branding_favicon }}">
<script type="module" src="{% static 'dist/rapidoc-min.js' %}"></script>
<script>
function getCookie(name) {
let cookieValue = "";
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 %}
{% 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 %}

View File

@ -56,7 +56,6 @@ from authentik.providers.oauth2.api.tokens import (
RefreshTokenViewSet,
)
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.providers import SAMLProviderViewSet
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/proxy", ProxyOutpostConfigViewSet)
router.register("outposts/ldap", LDAPOutpostConfigViewSet)
router.register("outposts/radius", RadiusOutpostConfigViewSet)
router.register("flows/instances", FlowViewSet)
router.register("flows/bindings", FlowStageBindingViewSet)
@ -168,7 +166,6 @@ router.register("providers/proxy", ProxyProviderViewSet)
router.register("providers/oauth2", OAuth2ProviderViewSet)
router.register("providers/saml", SAMLProviderViewSet)
router.register("providers/scim", SCIMProviderViewSet)
router.register("providers/radius", RadiusProviderViewSet)
router.register("oauth2/authorization_codes", AuthorizationCodeViewSet)
router.register("oauth2/refresh_tokens", RefreshTokenViewSet)

View File

@ -55,11 +55,11 @@ class AuthentikBlueprintsConfig(ManagedAppConfig):
"""Load v1 tasks"""
self.import_module("authentik.blueprints.v1.tasks")
def reconcile_blueprints_discovery(self):
def reconcile_blueprints_discover(self):
"""Run blueprint discovery"""
from authentik.blueprints.v1.tasks import blueprints_discovery, clear_failed_blueprints
from authentik.blueprints.v1.tasks import blueprints_discover, clear_failed_blueprints
blueprints_discovery.delay()
blueprints_discover.delay()
clear_failed_blueprints.delay()
def import_models(self):

View File

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

View File

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

View File

@ -5,7 +5,7 @@ from authentik.lib.utils.time import fqdn_rand
CELERY_BEAT_SCHEDULE = {
"blueprints_v1_discover": {
"task": "authentik.blueprints.v1.tasks.blueprints_discovery",
"task": "authentik.blueprints.v1.tasks.blueprints_discover",
"schedule": crontab(minute=fqdn_rand("blueprints_v1_discover"), hour="*"),
"options": {"queue": "authentik_scheduled"},
},

View File

@ -1,5 +1,6 @@
"""Blueprint helpers"""
from functools import wraps
from pathlib import Path
from typing import Callable
from django.apps import apps
@ -44,3 +45,13 @@ def reconcile_app(app_name: str):
return wrapper
return wrapper_outer
def load_yaml_fixture(path: str, **kwargs) -> str:
"""Load yaml fixture, optionally formatting it with kwargs"""
with open(Path(__file__).resolve().parent / Path(path), "r", encoding="utf-8") as _fixture:
fixture = _fixture.read()
try:
return fixture % kwargs
except TypeError:
return fixture

View File

@ -1,15 +1,34 @@
"""authentik managed models tests"""
from typing import Callable, Type
from django.apps import apps
from django.test import TestCase
from authentik.blueprints.models import BlueprintInstance, BlueprintRetrievalFailed
from authentik.lib.generators import generate_id
from authentik.blueprints.v1.importer import is_model_allowed
from authentik.lib.models import SerializerModel
class TestModels(TestCase):
"""Test Models"""
def test_retrieve_file(self):
"""Test retrieve_file"""
instance = BlueprintInstance.objects.create(name=generate_id(), path="../etc/hosts")
with self.assertRaises(BlueprintRetrievalFailed):
instance.retrieve()
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

@ -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

@ -3,12 +3,12 @@ from os import environ
from django.test import TransactionTestCase
from authentik.blueprints.tests import load_yaml_fixture
from authentik.blueprints.v1.exporter import FlowExporter
from authentik.blueprints.v1.importer import Importer, transaction_rollback
from authentik.core.models import Group
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding
from authentik.lib.generators import generate_id
from authentik.lib.tests.utils import load_fixture
from authentik.policies.expression.models import ExpressionPolicy
from authentik.policies.models import PolicyBinding
from authentik.sources.oauth.models import OAuthSource
@ -113,14 +113,14 @@ class TestBlueprintsV1(TransactionTestCase):
"""Test export and import it twice"""
count_initial = Prompt.objects.filter(field_key="username").count()
importer = Importer(load_fixture("fixtures/static_prompt_export.yaml"))
importer = Importer(load_yaml_fixture("fixtures/static_prompt_export.yaml"))
self.assertTrue(importer.validate()[0])
self.assertTrue(importer.apply())
count_before = Prompt.objects.filter(field_key="username").count()
self.assertEqual(count_initial + 1, count_before)
importer = Importer(load_fixture("fixtures/static_prompt_export.yaml"))
importer = Importer(load_yaml_fixture("fixtures/static_prompt_export.yaml"))
self.assertTrue(importer.apply())
self.assertEqual(Prompt.objects.filter(field_key="username").count(), count_before)
@ -130,7 +130,7 @@ class TestBlueprintsV1(TransactionTestCase):
ExpressionPolicy.objects.filter(name="foo-bar-baz-qux").delete()
Group.objects.filter(name="test").delete()
environ["foo"] = generate_id()
importer = Importer(load_fixture("fixtures/tags.yaml"), {"bar": "baz"})
importer = Importer(load_yaml_fixture("fixtures/tags.yaml"), {"bar": "baz"})
self.assertTrue(importer.validate()[0])
self.assertTrue(importer.apply())
policy = ExpressionPolicy.objects.filter(name="foo-bar-baz-qux").first()

View File

@ -1,10 +1,10 @@
"""Test blueprints v1"""
from django.test import TransactionTestCase
from authentik.blueprints.tests import load_yaml_fixture
from authentik.blueprints.v1.importer import Importer
from authentik.flows.models import Flow
from authentik.lib.generators import generate_id
from authentik.lib.tests.utils import load_fixture
class TestBlueprintsV1Conditions(TransactionTestCase):
@ -14,7 +14,7 @@ class TestBlueprintsV1Conditions(TransactionTestCase):
"""Test conditions fulfilled"""
flow_slug1 = generate_id()
flow_slug2 = generate_id()
import_yaml = load_fixture(
import_yaml = load_yaml_fixture(
"fixtures/conditions_fulfilled.yaml", id1=flow_slug1, id2=flow_slug2
)
@ -31,7 +31,7 @@ class TestBlueprintsV1Conditions(TransactionTestCase):
"""Test conditions not fulfilled"""
flow_slug1 = generate_id()
flow_slug2 = generate_id()
import_yaml = load_fixture(
import_yaml = load_yaml_fixture(
"fixtures/conditions_not_fulfilled.yaml", id1=flow_slug1, id2=flow_slug2
)

View File

@ -1,10 +1,10 @@
"""Test blueprints v1"""
from django.test import TransactionTestCase
from authentik.blueprints.tests import load_yaml_fixture
from authentik.blueprints.v1.importer import Importer
from authentik.flows.models import Flow
from authentik.lib.generators import generate_id
from authentik.lib.tests.utils import load_fixture
class TestBlueprintsV1State(TransactionTestCase):
@ -13,7 +13,7 @@ class TestBlueprintsV1State(TransactionTestCase):
def test_state_present(self):
"""Test state present"""
flow_slug = generate_id()
import_yaml = load_fixture("fixtures/state_present.yaml", id=flow_slug)
import_yaml = load_yaml_fixture("fixtures/state_present.yaml", id=flow_slug)
importer = Importer(import_yaml)
self.assertTrue(importer.validate()[0])
@ -39,7 +39,7 @@ class TestBlueprintsV1State(TransactionTestCase):
def test_state_created(self):
"""Test state created"""
flow_slug = generate_id()
import_yaml = load_fixture("fixtures/state_created.yaml", id=flow_slug)
import_yaml = load_yaml_fixture("fixtures/state_created.yaml", id=flow_slug)
importer = Importer(import_yaml)
self.assertTrue(importer.validate()[0])
@ -65,7 +65,7 @@ class TestBlueprintsV1State(TransactionTestCase):
def test_state_absent(self):
"""Test state absent"""
flow_slug = generate_id()
import_yaml = load_fixture("fixtures/state_created.yaml", id=flow_slug)
import_yaml = load_yaml_fixture("fixtures/state_created.yaml", id=flow_slug)
importer = Importer(import_yaml)
self.assertTrue(importer.validate()[0])
@ -74,7 +74,7 @@ class TestBlueprintsV1State(TransactionTestCase):
flow: Flow = Flow.objects.filter(slug=flow_slug).first()
self.assertEqual(flow.slug, flow_slug)
import_yaml = load_fixture("fixtures/state_absent.yaml", id=flow_slug)
import_yaml = load_yaml_fixture("fixtures/state_absent.yaml", id=flow_slug)
importer = Importer(import_yaml)
self.assertTrue(importer.validate()[0])
self.assertTrue(importer.apply())

View File

@ -6,7 +6,7 @@ from django.test import TransactionTestCase
from yaml import dump
from authentik.blueprints.models import BlueprintInstance, BlueprintInstanceStatus
from authentik.blueprints.v1.tasks import apply_blueprint, blueprints_discovery, blueprints_find
from authentik.blueprints.v1.tasks import apply_blueprint, blueprints_discover, blueprints_find
from authentik.lib.config import CONFIG
from authentik.lib.generators import generate_id
@ -53,7 +53,7 @@ class TestBlueprintsV1Tasks(TransactionTestCase):
file.seek(0)
file_hash = sha512(file.read().encode()).hexdigest()
file.flush()
blueprints_discovery() # pylint: disable=no-value-for-parameter
blueprints_discover() # pylint: disable=no-value-for-parameter
instance = BlueprintInstance.objects.filter(name=blueprint_id).first()
self.assertEqual(instance.last_applied_hash, file_hash)
self.assertEqual(
@ -81,7 +81,7 @@ class TestBlueprintsV1Tasks(TransactionTestCase):
)
)
file.flush()
blueprints_discovery() # pylint: disable=no-value-for-parameter
blueprints_discover() # pylint: disable=no-value-for-parameter
blueprint = BlueprintInstance.objects.filter(name="foo").first()
self.assertEqual(
blueprint.last_applied_hash,
@ -106,7 +106,7 @@ class TestBlueprintsV1Tasks(TransactionTestCase):
)
)
file.flush()
blueprints_discovery() # pylint: disable=no-value-for-parameter
blueprints_discover() # pylint: disable=no-value-for-parameter
blueprint.refresh_from_db()
self.assertEqual(
blueprint.last_applied_hash,

View File

@ -40,10 +40,6 @@ from authentik.lib.models import SerializerModel
from authentik.outposts.models import OutpostServiceConnection
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:
"""Check if model is allowed"""
@ -162,12 +158,7 @@ class Importer:
raise EntryInvalidError(f"Model {model} not allowed")
if issubclass(model, BaseMetaModel):
serializer_class: type[Serializer] = model.serializer()
serializer = serializer_class(
data=entry.get_attrs(self.__import),
context={
SERIALIZER_CONTEXT_BLUEPRINT: entry,
},
)
serializer = serializer_class(data=entry.get_attrs(self.__import))
try:
serializer.is_valid(raise_exception=True)
except ValidationError as exc:
@ -226,12 +217,7 @@ class Importer:
always_merger.merge(full_data, updated_identifiers)
serializer_kwargs["data"] = full_data
serializer: Serializer = model().serializer(
context={
SERIALIZER_CONTEXT_BLUEPRINT: entry,
},
**serializer_kwargs,
)
serializer: Serializer = model().serializer(**serializer_kwargs)
try:
serializer.is_valid(raise_exception=True)
except ValidationError as exc:

View File

@ -76,7 +76,7 @@ class BlueprintEventHandler(FileSystemEventHandler):
return
if isinstance(event, FileCreatedEvent):
LOGGER.debug("new blueprint file created, starting discovery")
blueprints_discovery.delay()
blueprints_discover.delay()
if isinstance(event, FileModifiedEvent):
path = Path(event.src_path)
root = Path(CONFIG.y("blueprints_dir")).absolute()
@ -122,7 +122,7 @@ def blueprints_find():
)
blueprint.meta = from_dict(BlueprintMetadata, metadata) if metadata else None
blueprints.append(blueprint)
LOGGER.debug(
LOGGER.info(
"parsed & loaded blueprint",
hash=file_hash,
path=str(path),
@ -134,7 +134,7 @@ def blueprints_find():
throws=(DatabaseError, ProgrammingError, InternalError), base=MonitoredTask, bind=True
)
@prefill_task
def blueprints_discovery(self: MonitoredTask):
def blueprints_discover(self: MonitoredTask):
"""Find blueprints and check if they need to be created in the database"""
count = 0
for blueprint in blueprints_find():

View File

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

View File

@ -16,7 +16,6 @@ from rest_framework.viewsets import ModelViewSet
from authentik.api.authorization import OwnerSuperuserPermissions
from authentik.api.decorators import permission_required
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.users import UserSerializer
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)
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]:
"""Ensure only API or App password tokens are created."""
request: Request = self.context.get("request")

View File

@ -67,12 +67,11 @@ from authentik.core.models import (
TokenIntents,
User,
)
from authentik.events.models import Event, EventAction
from authentik.events.models import EventAction
from authentik.flows.exceptions import FlowNonApplicableException
from authentik.flows.models import FlowToken
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner
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.tasks import send_mails
from authentik.stages.email.utils import TemplateEmailMessage
@ -212,9 +211,8 @@ class UserMetricsSerializer(PassiveSerializer):
def get_logins(self, _):
"""Get successful logins per 8 hours for the last 7 days"""
user = self.context["user"]
request = self.context["request"]
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
)
# 3 data points per day, so 8 hour spans
@ -225,9 +223,8 @@ class UserMetricsSerializer(PassiveSerializer):
def get_logins_failed(self, _):
"""Get failed logins per 8 hours for the last 7 days"""
user = self.context["user"]
request = self.context["request"]
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
)
# 3 data points per day, so 8 hour spans
@ -238,9 +235,8 @@ class UserMetricsSerializer(PassiveSerializer):
def get_authorizations(self, _):
"""Get failed logins per 8 hours for the last 7 days"""
user = self.context["user"]
request = self.context["request"]
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
)
# 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:
"""User metrics per 1h"""
user: User = self.get_object()
serializer = UserMetricsSerializer(instance={})
serializer = UserMetricsSerializer(True)
serializer.context["user"] = user
serializer.context["request"] = request
return Response(serializer.data)
@permission_required("authentik_core.reset_user_password")
@ -544,58 +539,6 @@ class UserViewSet(UsedByMixin, ModelViewSet):
send_mails(email_stage, message)
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:
"""Custom filter_queryset method which ignores guardian, but still supports sorting"""
for backend in list(self.filter_backends):

View File

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

View File

@ -21,14 +21,11 @@ PROPERTY_MAPPING_TIME = Histogram(
class PropertyMappingEvaluator(BaseEvaluator):
"""Custom Evaluator that adds some different context variables."""
dry_run: bool
def __init__(
self,
model: Model,
user: Optional[User] = None,
request: Optional[HttpRequest] = None,
dry_run: Optional[bool] = False,
**kwargs,
):
if hasattr(model, "name"):
@ -45,13 +42,9 @@ class PropertyMappingEvaluator(BaseEvaluator):
req.http_request = request
self._context["request"] = req
self._context.update(**kwargs)
self.dry_run = dry_run
def handle_error(self, exc: Exception, expression_source: str):
"""Exception Handler"""
# For dry-run requests we don't save exceptions
if self.dry_run:
return
error_string = exception_to_string(exc)
event = Event.new(
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):
"""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."))
path = models.TextField(default="users")
@ -249,17 +249,6 @@ class Provider(SerializerModel):
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(
"authentik_flows.Flow",
on_delete=models.CASCADE,

View File

@ -9,13 +9,16 @@
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<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="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 %}
{% endblock %}
<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/custom.css' %}" data-inject>
<script src="{% static 'dist/poly.js' %}?version={{ version }}" type="module"></script>
<script src="{% static 'dist/standalone/loading/index.js' %}?version={{ version }}" type="module"></script>
<script src="{% static 'dist/poly.js' %}" type="module"></script>
{% block head %}
{% endblock %}
<meta name="sentry-trace" content="{{ sentry_trace }}" />

View File

@ -1,6 +1,7 @@
{% extends "base/skeleton.html" %}
{% load static %}
{% load i18n %}
{% block head %}
<script src="{% static 'dist/admin/AdminInterface.js' %}?version={{ version }}" type="module"></script>
@ -14,6 +15,19 @@
{% block body %}
<ak-message-container></ak-message-container>
<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>
{% endblock %}

View File

@ -1,6 +1,7 @@
{% extends "base/skeleton.html" %}
{% load static %}
{% load i18n %}
{% block head_before %}
{{ block.super }}
@ -30,6 +31,19 @@ window.authentik.flow = {
{% block body %}
<ak-message-container></ak-message-container>
<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>
{% endblock %}

View File

@ -1,6 +1,7 @@
{% extends "base/skeleton.html" %}
{% load static %}
{% load i18n %}
{% block head %}
<script src="{% static 'dist/user/UserInterface.js' %}?version={{ version }}" type="module"></script>
@ -14,6 +15,19 @@
{% block body %}
<ak-message-container></ak-message-container>
<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>
{% endblock %}

View File

@ -43,14 +43,14 @@ class TestApplicationsAPI(APITestCase):
self.assertEqual(
self.client.patch(
reverse("authentik_api:application-detail", kwargs={"slug": self.allowed.slug}),
{"meta_launch_url": "https://%(username)s-test.test.goauthentik.io/%(username)s"},
{"meta_launch_url": "https://%(username)s.test.goauthentik.io/%(username)s"},
).status_code,
200,
)
self.allowed.refresh_from_db()
self.assertEqual(
self.allowed.get_launch_url(self.user),
f"https://{self.user.username}-test.test.goauthentik.io/{self.user.username}",
f"https://{self.user.username}.test.goauthentik.io/{self.user.username}",
)
def test_set_icon(self):
@ -129,7 +129,6 @@ class TestApplicationsAPI(APITestCase):
"provider_obj": {
"assigned_application_name": "allowed",
"assigned_application_slug": "allowed",
"authentication_flow": None,
"authorization_flow": str(self.provider.authorization_flow.pk),
"component": "ak-provider-oauth2-form",
"meta_model_name": "authentik_providers_oauth2.oauth2provider",
@ -179,7 +178,6 @@ class TestApplicationsAPI(APITestCase):
"provider_obj": {
"assigned_application_name": "allowed",
"assigned_application_slug": "allowed",
"authentication_flow": None,
"authorization_flow": str(self.provider.authorization_flow.pk),
"component": "ak-provider-oauth2-form",
"meta_model_name": "authentik_providers_oauth2.oauth2provider",

View File

@ -1,14 +1,14 @@
"""impersonation tests"""
from json import loads
from django.test.testcases import TestCase
from django.urls import reverse
from rest_framework.test import APITestCase
from authentik.core.models import User
from authentik.core.tests.utils import create_test_admin_user
class TestImpersonation(APITestCase):
class TestImpersonation(TestCase):
"""impersonation tests"""
def setUp(self) -> None:
@ -23,10 +23,10 @@ class TestImpersonation(APITestCase):
self.other_user.save()
self.client.force_login(self.user)
self.client.post(
self.client.get(
reverse(
"authentik_api:user-impersonate",
kwargs={"pk": self.other_user.pk},
"authentik_core:impersonate-init",
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["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_body = loads(response.content.decode())
@ -46,7 +46,9 @@ class TestImpersonation(APITestCase):
"""test impersonation without permissions"""
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_body = loads(response.content.decode())
@ -56,5 +58,5 @@ class TestImpersonation(APITestCase):
"""test un-impersonation without impersonating first"""
self.client.force_login(self.other_user)
response = self.client.get(reverse("authentik_api:user-impersonate-end"))
self.assertEqual(response.status_code, 204)
response = self.client.get(reverse("authentik_core:impersonate-end"))
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.models import PropertyMapping
from authentik.core.tests.utils import create_test_admin_user
from authentik.events.models import Event, EventAction
from authentik.lib.generators import generate_id
from authentik.policies.expression.models import ExpressionPolicy
class TestPropertyMappings(TestCase):
@ -15,24 +12,23 @@ class TestPropertyMappings(TestCase):
def setUp(self) -> None:
super().setUp()
self.user = create_test_admin_user()
self.factory = RequestFactory()
def test_expression(self):
"""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")
def test_expression_syntax(self):
"""Test expression syntax error"""
mapping = PropertyMapping.objects.create(name=generate_id(), expression="-")
mapping = PropertyMapping.objects.create(name="test", expression="-")
with self.assertRaises(PropertyMappingExpressionException):
mapping.evaluate(None, None)
def test_expression_error_general(self):
"""Test expression error"""
expr = "return aaa"
mapping = PropertyMapping.objects.create(name=generate_id(), expression=expr)
mapping = PropertyMapping.objects.create(name="test", expression=expr)
with self.assertRaises(PropertyMappingExpressionException):
mapping.evaluate(None, None)
events = Event.objects.filter(
@ -45,7 +41,7 @@ class TestPropertyMappings(TestCase):
"""Test expression error (with user and http request"""
expr = "return aaa"
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):
mapping.evaluate(get_anonymous_user(), request)
events = Event.objects.filter(
@ -56,23 +52,3 @@ class TestPropertyMappings(TestCase):
event = events.first()
self.assertEqual(event.user["username"], "AnonymousUser")
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()
component = CharField()
title = CharField(required=True)
title = CharField()
configure_url = CharField(required=False)
icon_url = CharField(required=False)

View File

@ -1,18 +1,14 @@
"""authentik URL Configuration"""
from channels.auth import AuthMiddleware
from channels.sessions import CookieMiddleware
from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.urls import path
from django.views.decorators.csrf import ensure_csrf_cookie
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.interface import FlowInterfaceView, InterfaceView
from authentik.core.views.session import EndSessionView
from authentik.root.asgi_middleware import SessionMiddleware
from authentik.root.messages.consumer import MessageConsumer
urlpatterns = [
path(
@ -28,6 +24,17 @@ urlpatterns = [
apps.RedirectToAppLaunch.as_view(),
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
path(
"if/admin/",
@ -57,12 +64,6 @@ urlpatterns = [
),
]
websocket_urlpatterns = [
path(
"ws/client/", CookieMiddleware(SessionMiddleware(AuthMiddleware(MessageConsumer.as_asgi())))
),
]
if settings.DEBUG:
urlpatterns += [
path("debug/policy/deny/", AccessDeniedView.as_view(), name="debug-policy-deny"),

View File

@ -12,19 +12,16 @@ from authentik.flows.challenge import (
RedirectChallenge,
)
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.stage import ChallengeStageView
from authentik.flows.views.executor import (
SESSION_KEY_APPLICATION_PRE,
SESSION_KEY_PLAN,
ToDefaultFlow,
)
from authentik.flows.views.executor import SESSION_KEY_PLAN
from authentik.lib.utils.urls import redirect_with_qs
from authentik.stages.consent.stage import (
PLAN_CONTEXT_CONSENT_HEADER,
PLAN_CONTEXT_CONSENT_PERMISSIONS,
)
from authentik.tenants.models import Tenant
class RedirectToAppLaunch(View):
@ -39,10 +36,10 @@ class RedirectToAppLaunch(View):
# Check if we're authenticated already, saves us the flow run
if request.user.is_authenticated:
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
# 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.allow_empty_flows = True
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.
`user` arguments optionally overrides user from requests."""
if request:
from authentik.flows.views.executor import QS_QUERY
self.context["http_request"] = {
"path": request.path,
"method": request.method,
"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"):
tenant: Tenant = request.tenant
# Because self.created only gets set on save, we can't use it's value here

View File

@ -41,7 +41,7 @@ class TaskResult:
def with_error(self, exc: Exception) -> "TaskResult":
"""Since errors might not always be pickle-able, set the traceback"""
self.messages.append(exception_to_string(exc))
self.messages.append(str(exc))
return self

View File

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

View File

@ -271,15 +271,6 @@ class ConfigurableStage(models.Model):
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):
"""Subclass of a standard Token, stores the currently active flow plan upon creation.
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 error in errors:
full_errors.setdefault(field, [])
field_error = {
"string": str(error),
}
if hasattr(error, "code"):
field_error["code"] = error.code
full_errors[field].append(field_error)
full_errors[field].append(
{
"string": str(error),
"code": error.code,
}
)
challenge_response.initial_data["response_errors"] = full_errors
if not challenge_response.is_valid():
self.logger.error(

View File

@ -2,13 +2,10 @@
from django.test import TestCase
from django.urls import reverse
from authentik.core.models import Application
from authentik.core.tests.utils import create_test_flow
from authentik.flows.models import Flow, FlowDesignation
from authentik.flows.planner import FlowPlan
from authentik.flows.views.executor import SESSION_KEY_APPLICATION_PRE, SESSION_KEY_PLAN
from authentik.lib.generators import generate_id
from authentik.providers.oauth2.models import OAuth2Provider
from authentik.flows.views.executor import SESSION_KEY_PLAN
class TestHelperView(TestCase):
@ -25,41 +22,6 @@ class TestHelperView(TestCase):
self.assertEqual(response.status_code, 302)
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):
"""Test that ToDefaultFlow returns the expected URL (with an invalid plan)"""
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 structlog.stdlib import BoundLogger, get_logger
from authentik.core.models import Application
from authentik.events.models import Event, EventAction, cleanse_dict
from authentik.flows.challenge import (
Challenge,
@ -69,7 +68,6 @@ SESSION_KEY_GET = "authentik/flows/get"
SESSION_KEY_POST = "authentik/flows/post"
SESSION_KEY_HISTORY = "authentik/flows/history"
QS_KEY_TOKEN = "flow_token" # nosec
QS_QUERY = "query"
def challenge_types():
@ -174,7 +172,7 @@ class FlowExecutorView(APIView):
op="authentik.flow.executor.dispatch", description=self.flow.slug
) as span:
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:
plan = self._check_flow_token(get_params[QS_KEY_TOKEN])
if plan:
@ -477,32 +475,20 @@ class ToDefaultFlow(View):
LOGGER.debug("flow_by_policy: no flow found", filters=flow_filter)
return None
def get_flow(self) -> Flow:
"""Get a flow for the selected designation"""
tenant: Tenant = self.request.tenant
def dispatch(self, request: HttpRequest) -> HttpResponse:
tenant: Tenant = request.tenant
flow = None
# First, attempt to get default flow from tenant
if self.designation == FlowDesignation.AUTHENTICATION:
flow = tenant.flow_authentication
# Check if we have a default flow from application
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:
if self.designation == FlowDesignation.INVALIDATION:
flow = tenant.flow_invalidation
if flow:
return flow
# If no flow was set, get the first based on slug and policy
flow = self.flow_by_policy(self.request, designation=self.designation)
if flow:
return flow
if not flow:
flow = self.flow_by_policy(request, designation=self.designation)
# If we still don't have a flow, 404
raise Http404
def dispatch(self, request: HttpRequest) -> HttpResponse:
flow = self.get_flow()
if not flow:
raise Http404
# If user already has a pending plan, clear it so we don't have to later.
if SESSION_KEY_PLAN in self.request.session:
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 django.core.exceptions import FieldError
from django_otp import devices_for_user
from guardian.shortcuts import get_anonymous_user
from rest_framework.serializers import ValidationError
from sentry_sdk.hub import Hub
from sentry_sdk.tracing import Span
@ -17,9 +16,7 @@ from structlog.stdlib import get_logger
from authentik.core.models import User
from authentik.events.models import Event
from authentik.lib.utils.http import get_http_session
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
LOGGER = get_logger()
@ -40,20 +37,19 @@ class BaseEvaluator:
# update website/docs/expressions/_objects.md
# update website/docs/expressions/_functions.md
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_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,
"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 = {}
@ -156,19 +152,6 @@ class BaseEvaluator:
return
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:
"""Wrap expression in a function, call it, and save the result as `result`"""
handler_signature = ",".join(params)

View File

@ -81,8 +81,7 @@ class DomainlessFormattedURLValidator(DomainlessURLValidator):
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self.formatter_re = r"([%\(\)a-zA-Z])*"
self.host_re = "(" + self.formatter_re + self.hostname_re + self.domain_re + "|localhost)"
self.host_re = r"([%\(\)a-zA-Z])+" + self.domain_re + self.domain_re
self.regex = _lazy_re_compile(
r"^(?:[a-z0-9.+-]*)://" # scheme is validated separately
r"(?:[^\s:@/]+(?::[^\s:@/]*)?@)?" # user:pass authentication

View File

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

View File

@ -1,7 +1,4 @@
"""Test utils"""
from inspect import currentframe
from pathlib import Path
from django.contrib.messages.middleware import MessageMiddleware
from django.contrib.sessions.middleware import SessionMiddleware
from django.http import HttpRequest
@ -14,21 +11,6 @@ def dummy_get_response(request: HttpRequest): # pragma: no cover
return None
def load_fixture(path: str, **kwargs) -> str:
"""Load fixture, optionally formatting it with kwargs"""
current = currentframe()
parent = current.f_back
calling_file_path = parent.f_globals["__file__"]
with open(
Path(calling_file_path).resolve().parent / Path(path), "r", encoding="utf-8"
) as _fixture:
fixture = _fixture.read()
try:
return fixture % kwargs
except TypeError:
return fixture
def get_request(*args, user=None, **kwargs):
"""Get a request with usable session"""
request = RequestFactory().get(*args, **kwargs)

View File

@ -38,17 +38,13 @@ def _get_outpost_override_ip(request: HttpRequest) -> Optional[str]:
if OUTPOST_REMOTE_IP_HEADER not in request.META or OUTPOST_TOKEN_HEADER not in request.META:
return None
fake_ip = request.META[OUTPOST_REMOTE_IP_HEADER]
token = (
Token.filter_not_expired(
key=request.META.get(OUTPOST_TOKEN_HEADER), intent=TokenIntents.INTENT_API
)
.select_related("user")
.first()
tokens = Token.filter_not_expired(
key=request.META.get(OUTPOST_TOKEN_HEADER), intent=TokenIntents.INTENT_API
)
if not token:
if not tokens.exists():
LOGGER.warning("Attempted remote-ip override without token", fake_ip=fake_ip)
return None
user = token.user
user = tokens.first().user
if not user.group_attributes(request).get(USER_ATTRIBUTE_CAN_OVERRIDE_IP, False):
LOGGER.warning(
"Remote-IP override: user doesn't have permission",

View File

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

View File

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

View File

@ -4,7 +4,6 @@ from typing import TYPE_CHECKING
from django.utils.text import slugify
from kubernetes.client import (
AppsV1Api,
V1Capabilities,
V1Container,
V1ContainerPort,
V1Deployment,
@ -14,12 +13,9 @@ from kubernetes.client import (
V1LabelSelector,
V1ObjectMeta,
V1ObjectReference,
V1PodSecurityContext,
V1PodSpec,
V1PodTemplateSpec,
V1SeccompProfile,
V1SecretKeySelector,
V1SecurityContext,
)
from authentik import __version__, get_full_version
@ -107,11 +103,6 @@ class DeploymentReconciler(KubernetesObjectReconciler[V1Deployment]):
image_pull_secrets=[
V1ObjectReference(name=secret) for secret in image_pull_secrets
],
security_context=V1PodSecurityContext(
seccomp_profile=V1SeccompProfile(
type="RuntimeDefault",
),
),
containers=[
V1Container(
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"
LDAP = "ldap"
RADIUS = "radius"
def default_outpost_config(host: Optional[str] = None):

View File

@ -19,9 +19,9 @@ CELERY_BEAT_SCHEDULE = {
"schedule": crontab(minute=fqdn_rand("outpost_token_ensurer"), hour="*/8"),
"options": {"queue": "authentik_scheduled"},
},
"outpost_connection_discovery": {
"task": "authentik.outposts.tasks.outpost_connection_discovery",
"schedule": crontab(minute=fqdn_rand("outpost_connection_discovery"), hour="*/8"),
"outpost_local_connection": {
"task": "authentik.outposts.tasks.outpost_local_connection",
"schedule": crontab(minute=fqdn_rand("outpost_local_connection"), hour="*/8"),
"options": {"queue": "authentik_scheduled"},
},
}

View File

@ -7,7 +7,6 @@ from urllib.parse import urlparse
import yaml
from asgiref.sync import async_to_sync
from channels.layers import get_channel_layer
from django.core.cache import cache
from django.db import DatabaseError, InternalError, ProgrammingError
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.kubernetes import ProxyKubernetesController
from authentik.root.celery import CELERY_APP
from authentik.root.messages.storage import closing_send
LOGGER = get_logger()
CACHE_KEY_OUTPOST_DOWN = "outpost_teardown_%s"
@ -214,58 +214,50 @@ def outpost_post_save(model_class: str, model_pk: Any):
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
instance they are connected"""
channel_layer = get_channel_layer()
if isinstance(model_instance, OutpostModel):
for outpost in model_instance.outpost_set.all():
_outpost_single_update(outpost, channel_layer)
elif isinstance(model_instance, Outpost):
_outpost_single_update(model_instance, channel_layer)
if isinstance(model_instace, OutpostModel):
for outpost in model_instace.outpost_set.all():
_outpost_single_update(outpost)
elif isinstance(model_instace, Outpost):
_outpost_single_update(model_instace)
def _outpost_single_update(outpost: Outpost, layer=None):
def _outpost_single_update(outpost: Outpost):
"""Update outpost instances connected to a single outpost"""
# Ensure token again, because this function is called when anything related to an
# OutpostModel is saved, so we can be sure permissions are right
_ = outpost.token
outpost.build_user_permissions(outpost.user)
if not layer: # pragma: no cover
layer = get_channel_layer()
for state in OutpostState.for_outpost(outpost):
for channel in state.channel_ids:
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(
base=MonitoredTask,
bind=True,
)
def outpost_connection_discovery(self: MonitoredTask):
@CELERY_APP.task()
def outpost_local_connection():
"""Checks the local environment and create Service connections."""
status = TaskResult(TaskResultStatus.SUCCESSFUL)
if not CONFIG.y_bool("outposts.discover"):
status.messages.append("Outpost integration discovery is disabled")
self.set_status(status)
LOGGER.info("Outpost integration discovery is disabled")
return
# Explicitly check against token filename, as that's
# only present when the integration is enabled
if Path(SERVICE_TOKEN_FILENAME).exists():
status.messages.append("Detected in-cluster Kubernetes Config")
LOGGER.info("Detected in-cluster Kubernetes Config")
if not KubernetesServiceConnection.objects.filter(local=True).exists():
status.messages.append("Created Service Connection for in-cluster")
LOGGER.debug("Created Service Connection for in-cluster")
KubernetesServiceConnection.objects.create(
name="Local Kubernetes Cluster", local=True, kubeconfig={}
)
# For development, check for the existence of a kubeconfig file
kubeconfig_path = Path(KUBE_CONFIG_DEFAULT_LOCATION).expanduser()
if kubeconfig_path.exists():
status.messages.append("Detected kubeconfig")
LOGGER.info("Detected kubeconfig")
kubeconfig_local_name = f"k8s-{gethostname()}"
if not KubernetesServiceConnection.objects.filter(name=kubeconfig_local_name).exists():
status.messages.append("Creating kubeconfig Service Connection")
LOGGER.debug("Creating kubeconfig Service Connection")
with kubeconfig_path.open("r", encoding="utf8") as _kubeconfig:
KubernetesServiceConnection.objects.create(
name=kubeconfig_local_name,
@ -274,12 +266,11 @@ def outpost_connection_discovery(self: MonitoredTask):
unix_socket_path = urlparse(DEFAULT_UNIX_SOCKET).path
socket = Path(unix_socket_path)
if socket.exists() and access(socket, R_OK):
status.messages.append("Detected local docker socket")
LOGGER.info("Detected local docker socket")
if len(DockerServiceConnection.objects.filter(local=True)) == 0:
status.messages.append("Created Service Connection for docker")
LOGGER.debug("Created Service Connection for docker")
DockerServiceConnection.objects.create(
name="Local Docker connection",
local=True,
url=unix_socket_path,
)
self.set_status(status)

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.utils.http import get_client_ip
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
LOGGER = get_logger()
@ -30,11 +32,22 @@ class PolicyEvaluator(BaseEvaluator):
# update website/docs/expressions/_functions.md
self._context["ak_message"] = self.expr_func_message
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):
"""Wrapper to append to messages list, which is returned with PolicyResult"""
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):
"""Update context based on policy request (if http request is given, update that too)"""
# update website/docs/expressions/_objects.md
@ -70,7 +83,6 @@ class PolicyEvaluator(BaseEvaluator):
return PolicyResult(False, str(exc))
else:
policy_result = PolicyResult(False, *self._messages)
policy_result.raw_result = result
if result is None:
LOGGER.warning(
"Expression policy returned None",

View File

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

View File

@ -132,9 +132,9 @@ class TestPolicyProcess(TestCase):
)
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.resolver_match = resolve(reverse("authentik_api:user-impersonate-end"))
http_request.resolver_match = resolve(reverse("authentik_core:impersonate-end"))
request = PolicyRequest(self.user)
request.set_http_request(http_request)

View File

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

View File

@ -26,7 +26,6 @@ class LDAPProviderSerializer(ProviderSerializer):
"search_mode",
"bind_mode",
]
extra_kwargs = ProviderSerializer.Meta.extra_kwargs
class LDAPProviderViewSet(UsedByMixin, ModelViewSet):

View File

@ -39,7 +39,6 @@ class OAuth2ProviderSerializer(ProviderSerializer):
"issuer_mode",
"jwks_sources",
]
extra_kwargs = ProviderSerializer.Meta.extra_kwargs
class OAuth2ProviderSetupURLs(PassiveSerializer):

View File

@ -3,8 +3,6 @@
import django.utils.timezone
from django.db import migrations, models
import authentik.providers.oauth2.models
class Migration(migrations.Migration):
dependencies = [
@ -39,14 +37,4 @@ class Migration(migrations.Migration):
),
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
def generate_client_secret() -> str:
"""Generate client secret with adequate length"""
return generate_id(128)
class ClientTypes(models.TextChoices):
"""Confidential clients are capable of maintaining the confidentiality
of their credentials. Public clients are incapable."""
@ -137,7 +132,7 @@ class OAuth2Provider(Provider):
max_length=255,
blank=True,
verbose_name=_("Client Secret"),
default=generate_client_secret,
default=generate_key,
)
redirect_uris = models.TextField(
default="",

View File

@ -7,6 +7,7 @@ from rest_framework.test import APITestCase
from authentik.blueprints.tests import apply_blueprint
from authentik.core.models import Application
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
@ -17,6 +18,8 @@ class TestAPI(APITestCase):
def setUp(self) -> None:
self.provider: OAuth2Provider = OAuth2Provider.objects.create(
name="test",
client_id=generate_id(),
client_secret=generate_key(),
authorization_flow=create_test_flow(),
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.events.models import Event, EventAction
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.providers.oauth2.constants import TOKEN_TYPE
from authentik.providers.oauth2.errors import AuthorizeError, ClientIdError, RedirectUriError
@ -298,6 +298,7 @@ class TestAuthorize(OAuthTestCase):
provider: OAuth2Provider = OAuth2Provider.objects.create(
name=generate_id(),
client_id="test",
client_secret=generate_key(),
authorization_flow=flow,
redirect_uris="http://localhost",
signing_key=self.keypair,
@ -354,67 +355,13 @@ class TestAuthorize(OAuthTestCase):
delta=5,
)
def test_full_fragment_code(self):
"""Test full authorization"""
flow = create_test_flow()
provider: OAuth2Provider = OAuth2Provider.objects.create(
name=generate_id(),
client_id="test",
authorization_flow=flow,
redirect_uris="http://localhost",
signing_key=self.keypair,
)
Application.objects.create(name="app", slug="app", provider=provider)
state = generate_id()
user = create_test_admin_user()
self.client.force_login(user)
with patch(
"authentik.providers.oauth2.id_token.get_login_event",
MagicMock(
return_value=Event(
action=EventAction.LOGIN,
context={PLAN_CONTEXT_METHOD: "password"},
created=now(),
)
),
):
# Step 1, initiate params and get redirect to flow
self.client.get(
reverse("authentik_providers_oauth2:authorize"),
data={
"response_type": "code",
"response_mode": "fragment",
"client_id": "test",
"state": state,
"scope": "openid",
"redirect_uri": "http://localhost",
"nonce": generate_id(),
},
)
response = self.client.get(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
)
code: AuthorizationCode = AuthorizationCode.objects.filter(user=user).first()
self.assertJSONEqual(
response.content.decode(),
{
"component": "xak-flow-redirect",
"type": ChallengeTypes.REDIRECT.value,
"to": (f"http://localhost#code={code.code}" f"&state={state}"),
},
)
self.assertAlmostEqual(
code.expires.timestamp() - now().timestamp(),
timedelta_from_string(provider.access_code_validity).total_seconds(),
delta=5,
)
def test_full_form_post_id_token(self):
"""Test full authorization (form_post response)"""
flow = create_test_flow()
provider = OAuth2Provider.objects.create(
name=generate_id(),
client_id=generate_id(),
client_secret=generate_key(),
authorization_flow=flow,
redirect_uris="http://localhost",
signing_key=self.keypair,
@ -464,6 +411,7 @@ class TestAuthorize(OAuthTestCase):
provider = OAuth2Provider.objects.create(
name=generate_id(),
client_id=generate_id(),
client_secret=generate_key(),
authorization_flow=flow,
redirect_uris="http://localhost",
signing_key=self.keypair,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -514,12 +514,7 @@ class OAuthFulfillmentStage(StageView):
return urlunsplit(uri)
if self.params.response_mode == ResponseMode.FRAGMENT:
query_fragment = {}
if self.params.grant_type in [GrantTypes.AUTHORIZATION_CODE]:
query_fragment["code"] = code.code
query_fragment["state"] = [str(self.params.state) if self.params.state else ""]
else:
query_fragment = self.create_implicit_response(code)
query_fragment = self.create_implicit_response(code)
uri = uri._replace(
fragment=uri.fragment + urlencode(query_fragment, doseq=True),

View File

@ -95,7 +95,6 @@ class ProxyProviderSerializer(ProviderSerializer):
"refresh_token_validity",
"outpost_set",
]
extra_kwargs = ProviderSerializer.Meta.extra_kwargs
class ProxyProviderViewSet(UsedByMixin, ModelViewSet):

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"

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