Compare commits

..

1 Commits

Author SHA1 Message Date
93b960a1f1 root: monitoring: force db connection reload before healthcheck
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-06-04 14:35:30 +02:00
725 changed files with 16648 additions and 34376 deletions

View File

@ -1,5 +1,5 @@
[bumpversion] [bumpversion]
current_version = 2024.6.1 current_version = 2024.4.2
tag = True tag = True
commit = True commit = True
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-(?P<rc_t>[a-zA-Z-]+)(?P<rc_n>[1-9]\\d*))? parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-(?P<rc_t>[a-zA-Z-]+)(?P<rc_n>[1-9]\\d*))?
@ -17,8 +17,6 @@ optional_value = final
[bumpversion:file:pyproject.toml] [bumpversion:file:pyproject.toml]
[bumpversion:file:package.json]
[bumpversion:file:docker-compose.yml] [bumpversion:file:docker-compose.yml]
[bumpversion:file:schema.yml] [bumpversion:file:schema.yml]

View File

@ -54,10 +54,9 @@ runs:
authentik: authentik:
outposts: outposts:
container_image_base: ghcr.io/goauthentik/dev-%(type)s:gh-%(build_hash)s container_image_base: ghcr.io/goauthentik/dev-%(type)s:gh-%(build_hash)s
global: image:
image: repository: ghcr.io/goauthentik/dev-server
repository: ghcr.io/goauthentik/dev-server tag: ${{ inputs.tag }}
tag: ${{ inputs.tag }}
``` ```
For arm64, use these values: For arm64, use these values:
@ -66,10 +65,9 @@ runs:
authentik: authentik:
outposts: outposts:
container_image_base: ghcr.io/goauthentik/dev-%(type)s:gh-%(build_hash)s container_image_base: ghcr.io/goauthentik/dev-%(type)s:gh-%(build_hash)s
global: image:
image: repository: ghcr.io/goauthentik/dev-server
repository: ghcr.io/goauthentik/dev-server tag: ${{ inputs.tag }}-arm64
tag: ${{ inputs.tag }}-arm64
``` ```
Afterwards, run the upgrade commands from the latest release notes. Afterwards, run the upgrade commands from the latest release notes.

View File

@ -21,10 +21,7 @@ updates:
labels: labels:
- dependencies - dependencies
- package-ecosystem: npm - package-ecosystem: npm
directories: directory: "/web"
- "/web"
- "/tests/wdio"
- "/web/sfe"
schedule: schedule:
interval: daily interval: daily
time: "04:00" time: "04:00"
@ -33,6 +30,7 @@ updates:
open-pull-requests-limit: 10 open-pull-requests-limit: 10
commit-message: commit-message:
prefix: "web:" prefix: "web:"
# TODO: deduplicate these groups
groups: groups:
sentry: sentry:
patterns: patterns:
@ -58,6 +56,38 @@ updates:
patterns: patterns:
- "@rollup/*" - "@rollup/*"
- "rollup-*" - "rollup-*"
- package-ecosystem: npm
directory: "/tests/wdio"
schedule:
interval: daily
time: "04:00"
labels:
- dependencies
open-pull-requests-limit: 10
commit-message:
prefix: "web:"
# TODO: deduplicate these groups
groups:
sentry:
patterns:
- "@sentry/*"
- "@spotlightjs/*"
babel:
patterns:
- "@babel/*"
- "babel-*"
eslint:
patterns:
- "@typescript-eslint/*"
- "eslint"
- "eslint-*"
storybook:
patterns:
- "@storybook/*"
- "*storybook*"
esbuild:
patterns:
- "@esbuild/*"
wdio: wdio:
patterns: patterns:
- "@wdio/*" - "@wdio/*"

View File

@ -31,12 +31,7 @@ jobs:
env: env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}
- name: Upgrade /web - name: Upgrade /web
working-directory: web working-directory: web/
run: |
export VERSION=`node -e 'console.log(require("../gen-ts-api/package.json").version)'`
npm i @goauthentik/api@$VERSION
- name: Upgrade /web/sfe
working-directory: web/sfe
run: | run: |
export VERSION=`node -e 'console.log(require("../gen-ts-api/package.json").version)'` export VERSION=`node -e 'console.log(require("../gen-ts-api/package.json").version)'`
npm i @goauthentik/api@$VERSION npm i @goauthentik/api@$VERSION

View File

@ -219,7 +219,7 @@ jobs:
with: with:
ref: ${{ github.event.pull_request.head.sha }} ref: ${{ github.event.pull_request.head.sha }}
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v3.1.0 uses: docker/setup-qemu-action@v3.0.0
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
- name: prepare variables - name: prepare variables
@ -240,7 +240,7 @@ jobs:
- name: generate ts client - name: generate ts client
run: make gen-client-ts run: make gen-client-ts
- name: Build Docker Image - name: Build Docker Image
uses: docker/build-push-action@v6 uses: docker/build-push-action@v5
with: with:
context: . context: .
secrets: | secrets: |

View File

@ -76,7 +76,7 @@ jobs:
with: with:
ref: ${{ github.event.pull_request.head.sha }} ref: ${{ github.event.pull_request.head.sha }}
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v3.1.0 uses: docker/setup-qemu-action@v3.0.0
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
- name: prepare variables - name: prepare variables
@ -96,7 +96,7 @@ jobs:
- name: Generate API - name: Generate API
run: make gen-client-go run: make gen-client-go
- name: Build Docker Image - name: Build Docker Image
uses: docker/build-push-action@v6 uses: docker/build-push-action@v5
with: with:
tags: ${{ steps.ev.outputs.imageTags }} tags: ${{ steps.ev.outputs.imageTags }}
file: ${{ matrix.type }}.Dockerfile file: ${{ matrix.type }}.Dockerfile

View File

@ -12,36 +12,14 @@ on:
- version-* - version-*
jobs: jobs:
lint: lint-eslint:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
command:
- lint
- lint:lockfile
- tsc
- prettier-check
project: project:
- web - web
- tests/wdio - tests/wdio
include:
- command: tsc
project: web
extra_setup: |
cd sfe/ && npm ci
- command: lit-analyse
project: web
extra_setup: |
# lit-analyse doesn't understand path rewrites, so make it
# belive it's an actual module
cd node_modules/@goauthentik
ln -s ../../src/ web
exclude:
- command: lint:lockfile
project: tests/wdio
- command: tsc
project: tests/wdio
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
@ -50,17 +28,85 @@ jobs:
cache: "npm" cache: "npm"
cache-dependency-path: ${{ matrix.project }}/package-lock.json cache-dependency-path: ${{ matrix.project }}/package-lock.json
- working-directory: ${{ matrix.project }}/ - working-directory: ${{ matrix.project }}/
run: | run: npm ci
npm ci
${{ matrix.extra_setup }}
- name: Generate API - name: Generate API
run: make gen-client-ts run: make gen-client-ts
- name: Lint - name: Eslint
working-directory: ${{ matrix.project }}/ working-directory: ${{ matrix.project }}/
run: npm run ${{ matrix.command }} run: npm run lint
lint-lockfile:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- working-directory: web/
run: |
[ -z "$(jq -r '.packages | to_entries[] | select((.key | startswith("node_modules")) and (.value | has("resolved") | not)) | .key' < package-lock.json)" ]
lint-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: web/package.json
cache: "npm"
cache-dependency-path: web/package-lock.json
- working-directory: web/
run: npm ci
- name: Generate API
run: make gen-client-ts
- name: TSC
working-directory: web/
run: npm run tsc
lint-prettier:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
project:
- web
- tests/wdio
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: ${{ matrix.project }}/package.json
cache: "npm"
cache-dependency-path: ${{ matrix.project }}/package-lock.json
- working-directory: ${{ matrix.project }}/
run: npm ci
- name: Generate API
run: make gen-client-ts
- name: prettier
working-directory: ${{ matrix.project }}/
run: npm run prettier-check
lint-lit-analyse:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: web/package.json
cache: "npm"
cache-dependency-path: web/package-lock.json
- working-directory: web/
run: |
npm ci
# lit-analyse doesn't understand path rewrites, so make it
# belive it's an actual module
cd node_modules/@goauthentik
ln -s ../../src/ web
- name: Generate API
run: make gen-client-ts
- name: lit-analyse
working-directory: web/
run: npm run lit-analyse
ci-web-mark: ci-web-mark:
needs: needs:
- lint - lint-lockfile
- lint-eslint
- lint-prettier
- lint-lit-analyse
- lint-build
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- run: echo mark - run: echo mark
@ -82,21 +128,3 @@ jobs:
- name: build - name: build
working-directory: web/ working-directory: web/
run: npm run build run: npm run build
test:
needs:
- ci-web-mark
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: web/package.json
cache: "npm"
cache-dependency-path: web/package-lock.json
- working-directory: web/
run: npm ci
- name: Generate API
run: make gen-client-ts
- name: test
working-directory: web/
run: npm run test

View File

@ -12,21 +12,27 @@ on:
- version-* - version-*
jobs: jobs:
lint: lint-lockfile:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
command:
- lint:lockfile
- prettier-check
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- working-directory: website/
run: |
[ -z "$(jq -r '.packages | to_entries[] | select((.key | startswith("node_modules")) and (.value | has("resolved") | not)) | .key' < package-lock.json)" ]
lint-prettier:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: website/package.json
cache: "npm"
cache-dependency-path: website/package-lock.json
- working-directory: website/ - working-directory: website/
run: npm ci run: npm ci
- name: Lint - name: prettier
working-directory: website/ working-directory: website/
run: npm run ${{ matrix.command }} run: npm run prettier-check
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
@ -63,7 +69,8 @@ jobs:
run: npm run ${{ matrix.job }} run: npm run ${{ matrix.job }}
ci-website-mark: ci-website-mark:
needs: needs:
- lint - lint-lockfile
- lint-prettier
- test - test
- build - build
runs-on: ubuntu-latest runs-on: ubuntu-latest

View File

@ -14,7 +14,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v3.1.0 uses: docker/setup-qemu-action@v3.0.0
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
- name: prepare variables - name: prepare variables
@ -40,7 +40,7 @@ jobs:
mkdir -p ./gen-ts-api mkdir -p ./gen-ts-api
mkdir -p ./gen-go-api mkdir -p ./gen-go-api
- name: Build Docker Image - name: Build Docker Image
uses: docker/build-push-action@v6 uses: docker/build-push-action@v5
with: with:
context: . context: .
push: true push: true
@ -68,7 +68,7 @@ jobs:
with: with:
go-version-file: "go.mod" go-version-file: "go.mod"
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v3.1.0 uses: docker/setup-qemu-action@v3.0.0
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
- name: prepare variables - name: prepare variables
@ -94,7 +94,7 @@ jobs:
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Build Docker Image - name: Build Docker Image
uses: docker/build-push-action@v6 uses: docker/build-push-action@v5
with: with:
push: true push: true
tags: ${{ steps.ev.outputs.imageTags }} tags: ${{ steps.ev.outputs.imageTags }}
@ -155,8 +155,8 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Run test suite in final docker images - name: Run test suite in final docker images
run: | run: |
echo "PG_PASS=$(openssl rand 32 | base64 -w 0)" >> .env echo "PG_PASS=$(openssl rand 32 | base64)" >> .env
echo "AUTHENTIK_SECRET_KEY=$(openssl rand 32 | base64 -w 0)" >> .env echo "AUTHENTIK_SECRET_KEY=$(openssl rand 32 | base64)" >> .env
docker compose pull -q docker compose pull -q
docker compose up --no-start docker compose up --no-start
docker compose start postgresql redis docker compose start postgresql redis

View File

@ -14,8 +14,8 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Pre-release test - name: Pre-release test
run: | run: |
echo "PG_PASS=$(openssl rand 32 | base64 -w 0)" >> .env echo "PG_PASS=$(openssl rand 32 | base64)" >> .env
echo "AUTHENTIK_SECRET_KEY=$(openssl rand 32 | base64 -w 0)" >> .env echo "AUTHENTIK_SECRET_KEY=$(openssl rand 32 | base64)" >> .env
docker buildx install docker buildx install
mkdir -p ./gen-ts-api mkdir -p ./gen-ts-api
docker build -t testing:latest . docker build -t testing:latest .

View File

@ -22,30 +22,20 @@ RUN npm run build-bundled
# Stage 2: Build webui # Stage 2: Build webui
FROM --platform=${BUILDPLATFORM} docker.io/node:22 as web-builder FROM --platform=${BUILDPLATFORM} docker.io/node:22 as web-builder
ARG GIT_BUILD_HASH
ENV GIT_BUILD_HASH=$GIT_BUILD_HASH
ENV NODE_ENV=production ENV NODE_ENV=production
WORKDIR /work/web WORKDIR /work/web
RUN --mount=type=bind,target=/work/web/package.json,src=./web/package.json \ RUN --mount=type=bind,target=/work/web/package.json,src=./web/package.json \
--mount=type=bind,target=/work/web/package-lock.json,src=./web/package-lock.json \ --mount=type=bind,target=/work/web/package-lock.json,src=./web/package-lock.json \
--mount=type=bind,target=/work/web/sfe/package.json,src=./web/sfe/package.json \
--mount=type=bind,target=/work/web/sfe/package-lock.json,src=./web/sfe/package-lock.json \
--mount=type=bind,target=/work/web/scripts,src=./web/scripts \
--mount=type=cache,id=npm-web,sharing=shared,target=/root/.npm \ --mount=type=cache,id=npm-web,sharing=shared,target=/root/.npm \
npm ci --include=dev && \
cd sfe && \
npm ci --include=dev npm ci --include=dev
COPY ./package.json /work
COPY ./web /work/web/ COPY ./web /work/web/
COPY ./website /work/website/ COPY ./website /work/website/
COPY ./gen-ts-api /work/web/node_modules/@goauthentik/api COPY ./gen-ts-api /work/web/node_modules/@goauthentik/api
RUN npm run build && \ RUN npm run build
cd sfe && \
npm run build
# Stage 3: Build go proxy # Stage 3: Build go proxy
FROM --platform=${BUILDPLATFORM} mcr.microsoft.com/oss/go/microsoft/golang:1.22-fips-bookworm AS go-builder FROM --platform=${BUILDPLATFORM} mcr.microsoft.com/oss/go/microsoft/golang:1.22-fips-bookworm AS go-builder

View File

@ -47,8 +47,8 @@ test-go:
go test -timeout 0 -v -race -cover ./... go test -timeout 0 -v -race -cover ./...
test-docker: ## Run all tests in a docker-compose test-docker: ## Run all tests in a docker-compose
echo "PG_PASS=$(shell openssl rand 32 | base64 -w 0)" >> .env echo "PG_PASS=$(shell openssl rand 32 | base64)" >> .env
echo "AUTHENTIK_SECRET_KEY=$(shell openssl rand 32 | base64 -w 0)" >> .env echo "AUTHENTIK_SECRET_KEY=$(shell openssl rand 32 | base64)" >> .env
docker compose pull -q docker compose pull -q
docker compose up --no-start docker compose up --no-start
docker compose start postgresql redis docker compose start postgresql redis
@ -60,11 +60,9 @@ test: ## Run the server tests and produce a coverage report (locally)
coverage html coverage html
coverage report coverage report
lint-fix: lint-codespell ## Lint and automatically fix errors in the python source code. Reports spelling errors. lint-fix: ## Lint and automatically fix errors in the python source code. Reports spelling errors.
black $(PY_SOURCES) black $(PY_SOURCES)
ruff check --fix $(PY_SOURCES) ruff check --fix $(PY_SOURCES)
lint-codespell: ## Reports spelling errors.
codespell -w $(CODESPELL_ARGS) codespell -w $(CODESPELL_ARGS)
lint: ## Lint the python and golang sources lint: ## Lint the python and golang sources
@ -241,7 +239,7 @@ website: website-lint-fix website-build ## Automatically fix formatting issues
website-install: website-install:
cd website && npm ci cd website && npm ci
website-lint-fix: lint-codespell website-lint-fix:
cd website && npm run prettier cd website && npm run prettier
website-build: website-build:

View File

@ -18,10 +18,10 @@ Even if the issue is not a CVE, we still greatly appreciate your help in hardeni
(.x being the latest patch release for each version) (.x being the latest patch release for each version)
| Version | Supported | | Version | Supported |
| -------- | --------- | | --------- | --------- |
| 2024.4.x | ✅ | | 2023.10.x | ✅ |
| 2024.6.x | ✅ | | 2024.2.x | ✅ |
## Reporting a Vulnerability ## Reporting a Vulnerability

View File

@ -2,7 +2,7 @@
from os import environ from os import environ
__version__ = "2024.6.1" __version__ = "2024.4.2"
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH" ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"

View File

@ -16,7 +16,6 @@ from rest_framework.views import APIView
from authentik import get_full_version from authentik import get_full_version
from authentik.core.api.utils import PassiveSerializer from authentik.core.api.utils import PassiveSerializer
from authentik.enterprise.license import LicenseKey
from authentik.lib.config import CONFIG from authentik.lib.config import CONFIG
from authentik.lib.utils.reflection import get_env from authentik.lib.utils.reflection import get_env
from authentik.outposts.apps import MANAGED_OUTPOST from authentik.outposts.apps import MANAGED_OUTPOST
@ -33,7 +32,7 @@ class RuntimeDict(TypedDict):
platform: str platform: str
uname: str uname: str
openssl_version: str openssl_version: str
openssl_fips_enabled: bool | None openssl_fips_mode: bool
authentik_version: str authentik_version: str
@ -72,9 +71,7 @@ class SystemInfoSerializer(PassiveSerializer):
"architecture": platform.machine(), "architecture": platform.machine(),
"authentik_version": get_full_version(), "authentik_version": get_full_version(),
"environment": get_env(), "environment": get_env(),
"openssl_fips_enabled": ( "openssl_fips_enabled": backend._fips_enabled,
backend._fips_enabled if LicenseKey.get_total().is_valid() else None
),
"openssl_version": OPENSSL_VERSION, "openssl_version": OPENSSL_VERSION,
"platform": platform.platform(), "platform": platform.platform(),
"python_version": python_version, "python_version": python_version,

View File

@ -1,13 +1,13 @@
{% extends "base/skeleton.html" %} {% extends "base/skeleton.html" %}
{% load authentik_core %} {% load static %}
{% block title %} {% block title %}
API Browser - {{ brand.branding_title }} API Browser - {{ brand.branding_title }}
{% endblock %} {% endblock %}
{% block head %} {% block head %}
{% versioned_script "dist/standalone/api-browser/index-%v.js" %} <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: light)">
<meta name="theme-color" content="#151515" media="(prefers-color-scheme: dark)"> <meta name="theme-color" content="#151515" media="(prefers-color-scheme: dark)">
{% endblock %} {% endblock %}

View File

@ -58,7 +58,7 @@ from authentik.outposts.models import OutpostServiceConnection
from authentik.policies.models import Policy, PolicyBindingModel from authentik.policies.models import Policy, PolicyBindingModel
from authentik.policies.reputation.models import Reputation from authentik.policies.reputation.models import Reputation
from authentik.providers.oauth2.models import AccessToken, AuthorizationCode, RefreshToken from authentik.providers.oauth2.models import AccessToken, AuthorizationCode, RefreshToken
from authentik.providers.scim.models import SCIMProviderGroup, SCIMProviderUser from authentik.providers.scim.models import SCIMGroup, SCIMUser
from authentik.sources.scim.models import SCIMSourceGroup, SCIMSourceUser from authentik.sources.scim.models import SCIMSourceGroup, SCIMSourceUser
from authentik.stages.authenticator_webauthn.models import WebAuthnDeviceType from authentik.stages.authenticator_webauthn.models import WebAuthnDeviceType
from authentik.tenants.models import Tenant from authentik.tenants.models import Tenant
@ -97,8 +97,8 @@ def excluded_models() -> list[type[Model]]:
# FIXME: these shouldn't need to be explicitly listed, but rather based off of a mixin # FIXME: these shouldn't need to be explicitly listed, but rather based off of a mixin
FlowToken, FlowToken,
LicenseUsage, LicenseUsage,
SCIMProviderGroup, SCIMGroup,
SCIMProviderUser, SCIMUser,
Tenant, Tenant,
SystemTask, SystemTask,
ConnectionToken, ConnectionToken,

View File

@ -11,20 +11,21 @@ from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.permissions import AllowAny from rest_framework.permissions import AllowAny
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer
from rest_framework.validators import UniqueValidator from rest_framework.validators import UniqueValidator
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from authentik.api.authorization import SecretKeyFilter from authentik.api.authorization import SecretKeyFilter
from authentik.brands.models import Brand from authentik.brands.models import Brand
from authentik.core.api.used_by import UsedByMixin from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer, PassiveSerializer from authentik.core.api.utils import PassiveSerializer
from authentik.tenants.utils import get_current_tenant from authentik.tenants.utils import get_current_tenant
class FooterLinkSerializer(PassiveSerializer): class FooterLinkSerializer(PassiveSerializer):
"""Links returned in Config API""" """Links returned in Config API"""
href = CharField(read_only=True, allow_null=True) href = CharField(read_only=True)
name = CharField(read_only=True) name = CharField(read_only=True)

View File

@ -17,6 +17,7 @@ from rest_framework.fields import CharField, ReadOnlyField, SerializerMethodFiel
from rest_framework.parsers import MultiPartParser from rest_framework.parsers import MultiPartParser
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
@ -25,7 +26,6 @@ from authentik.api.pagination import Pagination
from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT
from authentik.core.api.providers import ProviderSerializer from authentik.core.api.providers import ProviderSerializer
from authentik.core.api.used_by import UsedByMixin from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer
from authentik.core.models import Application, User from authentik.core.models import Application, User
from authentik.events.logs import LogEventSerializer, capture_logs from authentik.events.logs import LogEventSerializer, capture_logs
from authentik.events.models import EventAction from authentik.events.models import EventAction
@ -103,7 +103,7 @@ class ApplicationSerializer(ModelSerializer):
class ApplicationViewSet(UsedByMixin, ModelViewSet): class ApplicationViewSet(UsedByMixin, ModelViewSet):
"""Application Viewset""" """Application Viewset"""
queryset = Application.objects.all().prefetch_related("provider").prefetch_related("policies") queryset = Application.objects.all().prefetch_related("provider")
serializer_class = ApplicationSerializer serializer_class = ApplicationSerializer
search_fields = [ search_fields = [
"name", "name",

View File

@ -8,12 +8,12 @@ from rest_framework import mixins
from rest_framework.fields import SerializerMethodField from rest_framework.fields import SerializerMethodField
from rest_framework.filters import OrderingFilter, SearchFilter from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import GenericViewSet from rest_framework.viewsets import GenericViewSet
from ua_parser import user_agent_parser from ua_parser import user_agent_parser
from authentik.api.authorization import OwnerSuperuserPermissions from authentik.api.authorization import OwnerSuperuserPermissions
from authentik.core.api.used_by import UsedByMixin from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer
from authentik.core.models import AuthenticatedSession from authentik.core.models import AuthenticatedSession
from authentik.events.context_processors.asn import ASN_CONTEXT_PROCESSOR, ASNDict from authentik.events.context_processors.asn import ASN_CONTEXT_PROCESSOR, ASNDict
from authentik.events.context_processors.geoip import GEOIP_CONTEXT_PROCESSOR, GeoIPDict from authentik.events.context_processors.geoip import GEOIP_CONTEXT_PROCESSOR, GeoIPDict

View File

@ -2,7 +2,6 @@
from json import loads from json import loads
from django.db.models import Prefetch
from django.http import Http404 from django.http import Http404
from django_filters.filters import CharFilter, ModelMultipleChoiceFilter from django_filters.filters import CharFilter, ModelMultipleChoiceFilter
from django_filters.filterset import FilterSet from django_filters.filterset import FilterSet
@ -17,12 +16,12 @@ from rest_framework.decorators import action
from rest_framework.fields import CharField, IntegerField, SerializerMethodField from rest_framework.fields import CharField, IntegerField, SerializerMethodField
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.serializers import ListSerializer, ValidationError from rest_framework.serializers import ListSerializer, ModelSerializer, ValidationError
from rest_framework.validators import UniqueValidator from rest_framework.validators import UniqueValidator
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from authentik.core.api.used_by import UsedByMixin from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import JSONDictField, ModelSerializer, PassiveSerializer from authentik.core.api.utils import JSONDictField, PassiveSerializer
from authentik.core.models import Group, User from authentik.core.models import Group, User
from authentik.rbac.api.roles import RoleSerializer from authentik.rbac.api.roles import RoleSerializer
from authentik.rbac.decorators import permission_required from authentik.rbac.decorators import permission_required
@ -167,14 +166,8 @@ class GroupViewSet(UsedByMixin, ModelViewSet):
def get_queryset(self): def get_queryset(self):
base_qs = Group.objects.all().select_related("parent").prefetch_related("roles") base_qs = Group.objects.all().select_related("parent").prefetch_related("roles")
if self.serializer_class(context={"request": self.request})._should_include_users: if self.serializer_class(context={"request": self.request})._should_include_users:
base_qs = base_qs.prefetch_related("users") base_qs = base_qs.prefetch_related("users")
else:
base_qs = base_qs.prefetch_related(
Prefetch("users", queryset=User.objects.all().only("id"))
)
return base_qs return base_qs
@extend_schema( @extend_schema(

View File

@ -8,10 +8,11 @@ from guardian.shortcuts import get_objects_for_user
from rest_framework import mixins from rest_framework import mixins
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.exceptions import PermissionDenied from rest_framework.exceptions import PermissionDenied
from rest_framework.fields import BooleanField, CharField, SerializerMethodField from rest_framework.fields import BooleanField, CharField
from rest_framework.relations import PrimaryKeyRelatedField from rest_framework.relations import PrimaryKeyRelatedField
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer, SerializerMethodField
from rest_framework.viewsets import GenericViewSet from rest_framework.viewsets import GenericViewSet
from authentik.blueprints.api import ManagedSerializer from authentik.blueprints.api import ManagedSerializer
@ -19,7 +20,6 @@ from authentik.core.api.object_types import TypesMixin
from authentik.core.api.used_by import UsedByMixin from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ( from authentik.core.api.utils import (
MetaNameSerializer, MetaNameSerializer,
ModelSerializer,
PassiveSerializer, PassiveSerializer,
) )
from authentik.core.expression.evaluator import PropertyMappingEvaluator from authentik.core.expression.evaluator import PropertyMappingEvaluator
@ -80,10 +80,8 @@ class PropertyMappingViewSet(
class PropertyMappingTestSerializer(PolicyTestSerializer): class PropertyMappingTestSerializer(PolicyTestSerializer):
"""Test property mapping execution for a user/group with context""" """Test property mapping execution for a user/group with context"""
user = PrimaryKeyRelatedField(queryset=User.objects.all(), required=False, allow_null=True) user = PrimaryKeyRelatedField(queryset=User.objects.all(), required=False)
group = PrimaryKeyRelatedField( group = PrimaryKeyRelatedField(queryset=Group.objects.all(), required=False)
queryset=Group.objects.all(), required=False, allow_null=True
)
queryset = PropertyMapping.objects.select_subclasses() queryset = PropertyMapping.objects.select_subclasses()
serializer_class = PropertyMappingSerializer serializer_class = PropertyMappingSerializer

View File

@ -6,12 +6,13 @@ from django.utils.translation import gettext_lazy as _
from django_filters.filters import BooleanFilter from django_filters.filters import BooleanFilter
from django_filters.filterset import FilterSet from django_filters.filterset import FilterSet
from rest_framework import mixins from rest_framework import mixins
from rest_framework.fields import ReadOnlyField, SerializerMethodField from rest_framework.fields import ReadOnlyField
from rest_framework.serializers import ModelSerializer, SerializerMethodField
from rest_framework.viewsets import GenericViewSet from rest_framework.viewsets import GenericViewSet
from authentik.core.api.object_types import TypesMixin from authentik.core.api.object_types import TypesMixin
from authentik.core.api.used_by import UsedByMixin from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import MetaNameSerializer, ModelSerializer from authentik.core.api.utils import MetaNameSerializer
from authentik.core.models import Provider from authentik.core.models import Provider

View File

@ -11,6 +11,7 @@ from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.parsers import MultiPartParser from rest_framework.parsers import MultiPartParser
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import GenericViewSet from rest_framework.viewsets import GenericViewSet
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
@ -18,7 +19,7 @@ from authentik.api.authorization import OwnerFilter, OwnerSuperuserPermissions
from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT
from authentik.core.api.object_types import TypesMixin from authentik.core.api.object_types import TypesMixin
from authentik.core.api.used_by import UsedByMixin from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import MetaNameSerializer, ModelSerializer from authentik.core.api.utils import MetaNameSerializer
from authentik.core.models import Source, UserSourceConnection from authentik.core.models import Source, UserSourceConnection
from authentik.core.types import UserSettingSerializer from authentik.core.types import UserSettingSerializer
from authentik.lib.utils.file import ( from authentik.lib.utils.file import (

View File

@ -12,6 +12,7 @@ from rest_framework.fields import CharField
from rest_framework.filters import OrderingFilter, SearchFilter from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from authentik.api.authorization import OwnerSuperuserPermissions from authentik.api.authorization import OwnerSuperuserPermissions
@ -19,7 +20,7 @@ from authentik.blueprints.api import ManagedSerializer
from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT
from authentik.core.api.used_by import UsedByMixin from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.users import UserSerializer from authentik.core.api.users import UserSerializer
from authentik.core.api.utils import ModelSerializer, PassiveSerializer from authentik.core.api.utils import PassiveSerializer
from authentik.core.models import ( from authentik.core.models import (
USER_ATTRIBUTE_TOKEN_EXPIRING, USER_ATTRIBUTE_TOKEN_EXPIRING,
USER_ATTRIBUTE_TOKEN_MAXIMUM_LIFETIME, USER_ATTRIBUTE_TOKEN_MAXIMUM_LIFETIME,
@ -44,13 +45,6 @@ class TokenSerializer(ManagedSerializer, ModelSerializer):
if SERIALIZER_CONTEXT_BLUEPRINT in self.context: if SERIALIZER_CONTEXT_BLUEPRINT in self.context:
self.fields["key"] = CharField(required=False) self.fields["key"] = CharField(required=False)
def validate_user(self, user: User):
"""Ensure user of token cannot be changed"""
if self.instance and self.instance.user_id:
if user.pk != self.instance.user_id:
raise ValidationError("User cannot be changed")
return user
def validate(self, attrs: dict[Any, str]) -> dict[Any, str]: def validate(self, attrs: dict[Any, str]) -> dict[Any, str]:
"""Ensure only API or App password tokens are created.""" """Ensure only API or App password tokens are created."""
request: Request = self.context.get("request") request: Request = self.context.get("request")

View File

@ -39,12 +39,12 @@ def get_delete_action(manager: Manager) -> str:
"""Get the delete action from the Foreign key, falls back to cascade""" """Get the delete action from the Foreign key, falls back to cascade"""
if hasattr(manager, "field"): if hasattr(manager, "field"):
if manager.field.remote_field.on_delete.__name__ == SET_NULL.__name__: if manager.field.remote_field.on_delete.__name__ == SET_NULL.__name__:
return DeleteAction.SET_NULL.value return DeleteAction.SET_NULL.name
if manager.field.remote_field.on_delete.__name__ == SET_DEFAULT.__name__: if manager.field.remote_field.on_delete.__name__ == SET_DEFAULT.__name__:
return DeleteAction.SET_DEFAULT.value return DeleteAction.SET_DEFAULT.name
if hasattr(manager, "source_field"): if hasattr(manager, "source_field"):
return DeleteAction.CASCADE_MANY.value return DeleteAction.CASCADE_MANY.name
return DeleteAction.CASCADE.value return DeleteAction.CASCADE.name
class UsedByMixin: class UsedByMixin:

View File

@ -40,6 +40,7 @@ from rest_framework.serializers import (
BooleanField, BooleanField,
DateTimeField, DateTimeField,
ListSerializer, ListSerializer,
ModelSerializer,
PrimaryKeyRelatedField, PrimaryKeyRelatedField,
ValidationError, ValidationError,
) )
@ -51,12 +52,7 @@ from authentik.admin.api.metrics import CoordinateSerializer
from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT
from authentik.brands.models import Brand from authentik.brands.models import Brand
from authentik.core.api.used_by import UsedByMixin from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ( from authentik.core.api.utils import JSONDictField, LinkSerializer, PassiveSerializer
JSONDictField,
LinkSerializer,
ModelSerializer,
PassiveSerializer,
)
from authentik.core.middleware import ( from authentik.core.middleware import (
SESSION_KEY_IMPERSONATE_ORIGINAL_USER, SESSION_KEY_IMPERSONATE_ORIGINAL_USER,
SESSION_KEY_IMPERSONATE_USER, SESSION_KEY_IMPERSONATE_USER,

View File

@ -12,12 +12,9 @@ from rest_framework.fields import (
JSONField, JSONField,
SerializerMethodField, SerializerMethodField,
) )
from rest_framework.serializers import ModelSerializer as BaseModelSerializer
from rest_framework.serializers import ( from rest_framework.serializers import (
Serializer, Serializer,
ValidationError, ValidationError,
model_meta,
raise_errors_on_nested_writes,
) )
@ -28,39 +25,6 @@ def is_dict(value: Any):
raise ValidationError("Value must be a dictionary, and not have any duplicate keys.") raise ValidationError("Value must be a dictionary, and not have any duplicate keys.")
class ModelSerializer(BaseModelSerializer):
def update(self, instance: Model, validated_data):
raise_errors_on_nested_writes("update", self, validated_data)
info = model_meta.get_field_info(instance)
# Simply set each attribute on the instance, and then save it.
# Note that unlike `.create()` we don't need to treat many-to-many
# relationships as being a special case. During updates we already
# have an instance pk for the relationships to be associated with.
m2m_fields = []
for attr, value in validated_data.items():
if attr in info.relations and info.relations[attr].to_many:
m2m_fields.append((attr, value))
else:
setattr(instance, attr, value)
instance.save()
# Note that many-to-many fields are set after updating instance.
# Setting m2m fields triggers signals which could potentially change
# updated instance and we do not want it to collide with .update()
for attr, value in m2m_fields:
field = getattr(instance, attr)
# We can't check for inheritance here as m2m managers are generated dynamically
if field.__class__.__name__ == "RelatedManager":
field.set(value, bulk=False)
else:
field.set(value)
return instance
class JSONDictField(JSONField): class JSONDictField(JSONField):
"""JSON Field which only allows dictionaries""" """JSON Field which only allows dictionaries"""

View File

@ -76,11 +76,8 @@ class PropertyMappingEvaluator(BaseEvaluator):
) )
if "request" in self._context: if "request" in self._context:
req: PolicyRequest = self._context["request"] req: PolicyRequest = self._context["request"]
if req.http_request: event.from_http(req.http_request, req.user)
event.from_http(req.http_request, req.user) return
return
elif req.user:
event.set_user(req.user)
event.save() event.save()
def evaluate(self, *args, **kwargs) -> Any: def evaluate(self, *args, **kwargs) -> Any:

View File

@ -1,6 +1,5 @@
"""authentik core exceptions""" """authentik core exceptions"""
from authentik.lib.expression.exceptions import ControlFlowException
from authentik.lib.sentry import SentryIgnoredException from authentik.lib.sentry import SentryIgnoredException
@ -13,7 +12,7 @@ class PropertyMappingExpressionException(SentryIgnoredException):
self.mapping = mapping self.mapping = mapping
class SkipObjectException(ControlFlowException): class SkipObjectException(PropertyMappingExpressionException):
"""Exception which can be raised in a property mapping to skip syncing an object. """Exception which can be raised in a property mapping to skip syncing an object.
Only applies to Property mappings which sync objects, and not on mappings which transitively Only applies to Property mappings which sync objects, and not on mappings which transitively
apply to a single user""" apply to a single user"""

View File

@ -7,13 +7,12 @@ from django.db.backends.base.schema import BaseDatabaseSchemaEditor
def backport_is_backchannel(apps: Apps, schema_editor: BaseDatabaseSchemaEditor): def backport_is_backchannel(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
db_alias = schema_editor.connection.alias
from authentik.providers.ldap.models import LDAPProvider from authentik.providers.ldap.models import LDAPProvider
from authentik.providers.scim.models import SCIMProvider from authentik.providers.scim.models import SCIMProvider
for model in [LDAPProvider, SCIMProvider]: for model in [LDAPProvider, SCIMProvider]:
try: try:
for obj in model.objects.using(db_alias).only("is_backchannel"): for obj in model.objects.only("is_backchannel"):
obj.is_backchannel = True obj.is_backchannel = True
obj.save() obj.save()
except (DatabaseError, InternalError, ProgrammingError): except (DatabaseError, InternalError, ProgrammingError):

View File

@ -26,7 +26,6 @@ from authentik.blueprints.models import ManagedModel
from authentik.core.expression.exceptions import PropertyMappingExpressionException from authentik.core.expression.exceptions import PropertyMappingExpressionException
from authentik.core.types import UILoginButton, UserSettingSerializer from authentik.core.types import UILoginButton, UserSettingSerializer
from authentik.lib.avatars import get_avatar from authentik.lib.avatars import get_avatar
from authentik.lib.expression.exceptions import ControlFlowException
from authentik.lib.generators import generate_id from authentik.lib.generators import generate_id
from authentik.lib.models import ( from authentik.lib.models import (
CreatedUpdatedModel, CreatedUpdatedModel,
@ -784,8 +783,6 @@ class PropertyMapping(SerializerModel, ManagedModel):
evaluator = PropertyMappingEvaluator(self, user, request, **kwargs) evaluator = PropertyMappingEvaluator(self, user, request, **kwargs)
try: try:
return evaluator.evaluate(self.expression) return evaluator.evaluate(self.expression)
except ControlFlowException as exc:
raise exc
except Exception as exc: except Exception as exc:
raise PropertyMappingExpressionException(self, exc) from exc raise PropertyMappingExpressionException(self, exc) from exc

View File

@ -212,7 +212,7 @@ class SourceFlowManager:
def _prepare_flow( def _prepare_flow(
self, self,
flow: Flow | None, flow: Flow,
connection: UserSourceConnection, connection: UserSourceConnection,
stages: list[StageView] | None = None, stages: list[StageView] | None = None,
**kwargs, **kwargs,
@ -309,9 +309,7 @@ class SourceFlowManager:
# When request isn't authenticated we jump straight to auth # When request isn't authenticated we jump straight to auth
if not self.request.user.is_authenticated: if not self.request.user.is_authenticated:
return self.handle_auth(connection) return self.handle_auth(connection)
if SESSION_KEY_OVERRIDE_FLOW_TOKEN in self.request.session: # Connection has already been saved
return self._prepare_flow(None, connection)
connection.save()
Event.new( Event.new(
EventAction.SOURCE_LINKED, EventAction.SOURCE_LINKED,
message="Linked Source", message="Linked Source",

View File

@ -10,7 +10,7 @@
versionSubdomain: "{{ version_subdomain }}", versionSubdomain: "{{ version_subdomain }}",
build: "{{ build }}", build: "{{ build }}",
}; };
window.addEventListener("DOMContentLoaded", function () { window.addEventListener("DOMContentLoaded", () => {
{% for message in messages %} {% for message in messages %}
window.dispatchEvent( window.dispatchEvent(
new CustomEvent("ak-message", { new CustomEvent("ak-message", {

View File

@ -1,6 +1,5 @@
{% load static %} {% load static %}
{% load i18n %} {% load i18n %}
{% load authentik_core %}
<!DOCTYPE html> <!DOCTYPE html>
@ -15,8 +14,8 @@
{% endblock %} {% endblock %}
<link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}"> <link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}">
<link rel="stylesheet" type="text/css" href="{% static 'dist/custom.css' %}" data-inject> <link rel="stylesheet" type="text/css" href="{% static 'dist/custom.css' %}" data-inject>
{% versioned_script "dist/poly-%v.js" %} <script src="{% static 'dist/poly.js' %}?version={{ version }}" type="module"></script>
{% versioned_script "dist/standalone/loading/index-%v.js" %} <script src="{% static 'dist/standalone/loading/index.js' %}?version={{ version }}" type="module"></script>
{% block head %} {% block head %}
{% endblock %} {% endblock %}
<meta name="sentry-trace" content="{{ sentry_trace }}" /> <meta name="sentry-trace" content="{{ sentry_trace }}" />

View File

@ -1,9 +1,9 @@
{% extends "base/skeleton.html" %} {% extends "base/skeleton.html" %}
{% load authentik_core %} {% load static %}
{% block head %} {% block head %}
{% versioned_script "dist/admin/AdminInterface-%v.js" %} <script src="{% static 'dist/admin/AdminInterface.js' %}?version={{ version }}" type="module"></script>
<meta name="theme-color" content="#18191a" media="(prefers-color-scheme: dark)"> <meta name="theme-color" content="#18191a" media="(prefers-color-scheme: dark)">
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)"> <meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">
{% include "base/header_js.html" %} {% include "base/header_js.html" %}

View File

@ -1,7 +1,6 @@
{% extends "base/skeleton.html" %} {% extends "base/skeleton.html" %}
{% load static %} {% load static %}
{% load authentik_core %}
{% block head_before %} {% block head_before %}
{{ block.super }} {{ block.super }}
@ -18,7 +17,7 @@ window.authentik.flow = {
{% endblock %} {% endblock %}
{% block head %} {% block head %}
{% versioned_script "dist/flow/FlowInterface-%v.js" %} <script src="{% static 'dist/flow/FlowInterface.js' %}?version={{ version }}" type="module"></script>
<style> <style>
:root { :root {
--ak-flow-background: url("{{ flow.background_url }}"); --ak-flow-background: url("{{ flow.background_url }}");

View File

@ -1,9 +1,9 @@
{% extends "base/skeleton.html" %} {% extends "base/skeleton.html" %}
{% load authentik_core %} {% load static %}
{% block head %} {% block head %}
{% versioned_script "dist/user/UserInterface-%v.js" %} <script src="{% static 'dist/user/UserInterface.js' %}?version={{ version }}" type="module"></script>
<meta name="theme-color" content="#1c1e21" media="(prefers-color-scheme: light)"> <meta name="theme-color" content="#1c1e21" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#1c1e21" media="(prefers-color-scheme: dark)"> <meta name="theme-color" content="#1c1e21" media="(prefers-color-scheme: dark)">
{% include "base/header_js.html" %} {% include "base/header_js.html" %}

View File

@ -71,9 +71,9 @@
</li> </li>
{% endfor %} {% endfor %}
<li> <li>
<span> <a href="https://goauthentik.io?utm_source=authentik">
{% trans 'Powered by authentik' %} {% trans 'Powered by authentik' %}
</span> </a>
</li> </li>
</ul> </ul>
</footer> </footer>

View File

@ -1,21 +0,0 @@
"""authentik core tags"""
from django import template
from django.templatetags.static import static as static_loader
from django.utils.safestring import mark_safe
from authentik import get_full_version
register = template.Library()
@register.simple_tag()
def versioned_script(path: str) -> str:
"""Wrapper around {% static %} tag that supports setting the version"""
returned_lines = [
(
f'<script src="{static_loader(path.replace("%v", get_full_version()))}'
'" type="module"></script>'
),
]
return mark_safe("".join(returned_lines)) # nosec

View File

@ -3,10 +3,7 @@
from django.test import RequestFactory, TestCase from django.test import RequestFactory, TestCase
from guardian.shortcuts import get_anonymous_user from guardian.shortcuts import get_anonymous_user
from authentik.core.expression.exceptions import ( from authentik.core.expression.exceptions import PropertyMappingExpressionException
PropertyMappingExpressionException,
SkipObjectException,
)
from authentik.core.models import PropertyMapping from authentik.core.models import PropertyMapping
from authentik.core.tests.utils import create_test_admin_user from authentik.core.tests.utils import create_test_admin_user
from authentik.events.models import Event, EventAction from authentik.events.models import Event, EventAction
@ -45,17 +42,6 @@ class TestPropertyMappings(TestCase):
self.assertTrue(events.exists()) self.assertTrue(events.exists())
self.assertEqual(len(events), 1) self.assertEqual(len(events), 1)
def test_expression_skip(self):
"""Test expression error"""
expr = "raise SkipObject"
mapping = PropertyMapping.objects.create(name=generate_id(), expression=expr)
with self.assertRaises(SkipObjectException):
mapping.evaluate(None, None)
events = Event.objects.filter(
action=EventAction.PROPERTY_MAPPING_EXCEPTION, context__expression=expr
)
self.assertFalse(events.exists())
def test_expression_error_extended(self): def test_expression_error_extended(self):
"""Test expression error (with user and http request""" """Test expression error (with user and http request"""
expr = "return aaa" expr = "return aaa"

View File

@ -13,8 +13,9 @@ from authentik.core.models import (
USER_ATTRIBUTE_TOKEN_MAXIMUM_LIFETIME, USER_ATTRIBUTE_TOKEN_MAXIMUM_LIFETIME,
Token, Token,
TokenIntents, TokenIntents,
User,
) )
from authentik.core.tests.utils import create_test_admin_user, create_test_user from authentik.core.tests.utils import create_test_admin_user
from authentik.lib.generators import generate_id from authentik.lib.generators import generate_id
@ -23,7 +24,7 @@ class TestTokenAPI(APITestCase):
def setUp(self) -> None: def setUp(self) -> None:
super().setUp() super().setUp()
self.user = create_test_user() self.user = User.objects.create(username="testuser")
self.admin = create_test_admin_user() self.admin = create_test_admin_user()
self.client.force_login(self.user) self.client.force_login(self.user)
@ -153,24 +154,6 @@ class TestTokenAPI(APITestCase):
self.assertEqual(token.expiring, True) self.assertEqual(token.expiring, True)
self.assertNotEqual(token.expires.timestamp(), expires.timestamp()) self.assertNotEqual(token.expires.timestamp(), expires.timestamp())
def test_token_change_user(self):
"""Test creating a token and then changing the user"""
ident = generate_id()
response = self.client.post(reverse("authentik_api:token-list"), {"identifier": ident})
self.assertEqual(response.status_code, 201)
token = Token.objects.get(identifier=ident)
self.assertEqual(token.user, self.user)
self.assertEqual(token.intent, TokenIntents.INTENT_API)
self.assertEqual(token.expiring, True)
self.assertTrue(self.user.has_perm("authentik_core.view_token_key", token))
response = self.client.put(
reverse("authentik_api:token-detail", kwargs={"identifier": ident}),
data={"identifier": "user_token_poc_v3", "intent": "api", "user": self.admin.pk},
)
self.assertEqual(response.status_code, 400)
token.refresh_from_db()
self.assertEqual(token.user, self.user)
def test_list(self): def test_list(self):
"""Test Token List (Test normal authentication)""" """Test Token List (Test normal authentication)"""
Token.objects.all().delete() Token.objects.all().delete()

View File

@ -20,9 +20,8 @@ from authentik.core.api.transactional_applications import TransactionalApplicati
from authentik.core.api.users import UserViewSet from authentik.core.api.users import UserViewSet
from authentik.core.views import apps from authentik.core.views import apps
from authentik.core.views.debug import AccessDeniedView from authentik.core.views.debug import AccessDeniedView
from authentik.core.views.interface import InterfaceView from authentik.core.views.interface import FlowInterfaceView, InterfaceView
from authentik.core.views.session import EndSessionView from authentik.core.views.session import EndSessionView
from authentik.flows.views.interface import FlowInterfaceView
from authentik.root.asgi_middleware import SessionMiddleware from authentik.root.asgi_middleware import SessionMiddleware
from authentik.root.messages.consumer import MessageConsumer from authentik.root.messages.consumer import MessageConsumer
from authentik.root.middleware import ChannelsLoggingMiddleware from authentik.root.middleware import ChannelsLoggingMiddleware
@ -54,8 +53,6 @@ urlpatterns = [
), ),
path( path(
"if/flow/<slug:flow_slug>/", "if/flow/<slug:flow_slug>/",
# FIXME: move this url to the flows app...also will cause all
# of the reverse calls to be adjusted
ensure_csrf_cookie(FlowInterfaceView.as_view()), ensure_csrf_cookie(FlowInterfaceView.as_view()),
name="if-flow", name="if-flow",
), ),

View File

@ -8,6 +8,7 @@ from django.views import View
from authentik.core.models import Application from authentik.core.models import Application
from authentik.flows.challenge import ( from authentik.flows.challenge import (
ChallengeResponse, ChallengeResponse,
ChallengeTypes,
HttpChallengeResponse, HttpChallengeResponse,
RedirectChallenge, RedirectChallenge,
) )
@ -73,6 +74,7 @@ class RedirectToAppStage(ChallengeStageView):
raise Http404 raise Http404
return RedirectChallenge( return RedirectChallenge(
instance={ instance={
"type": ChallengeTypes.REDIRECT.value,
"to": launch, "to": launch,
} }
) )

View File

@ -3,6 +3,7 @@
from json import dumps from json import dumps
from typing import Any from typing import Any
from django.shortcuts import get_object_or_404
from django.views.generic.base import TemplateView from django.views.generic.base import TemplateView
from rest_framework.request import Request from rest_framework.request import Request
@ -10,6 +11,7 @@ from authentik import get_build_hash
from authentik.admin.tasks import LOCAL_VERSION from authentik.admin.tasks import LOCAL_VERSION
from authentik.api.v3.config import ConfigView from authentik.api.v3.config import ConfigView
from authentik.brands.api import CurrentBrandSerializer from authentik.brands.api import CurrentBrandSerializer
from authentik.flows.models import Flow
class InterfaceView(TemplateView): class InterfaceView(TemplateView):
@ -23,3 +25,14 @@ class InterfaceView(TemplateView):
kwargs["build"] = get_build_hash() kwargs["build"] = get_build_hash()
kwargs["url_kwargs"] = self.kwargs kwargs["url_kwargs"] = self.kwargs
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)
class FlowInterfaceView(InterfaceView):
"""Flow interface"""
template_name = "if/flow.html"
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
kwargs["flow"] = get_object_or_404(Flow, slug=self.kwargs.get("flow_slug"))
kwargs["inspector"] = "inspector" in self.request.GET
return super().get_context_data(**kwargs)

View File

@ -24,12 +24,13 @@ from rest_framework.fields import (
from rest_framework.filters import OrderingFilter, SearchFilter from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from authentik.api.authorization import SecretKeyFilter from authentik.api.authorization import SecretKeyFilter
from authentik.core.api.used_by import UsedByMixin from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer, PassiveSerializer from authentik.core.api.utils import PassiveSerializer
from authentik.crypto.apps import MANAGED_KEY from authentik.crypto.apps import MANAGED_KEY
from authentik.crypto.builder import CertificateBuilder, PrivateKeyAlg from authentik.crypto.builder import CertificateBuilder, PrivateKeyAlg
from authentik.crypto.models import CertificateKeyPair from authentik.crypto.models import CertificateKeyPair

View File

@ -241,7 +241,7 @@ class TestCrypto(APITestCase):
"model_name": "oauth2provider", "model_name": "oauth2provider",
"pk": str(provider.pk), "pk": str(provider.pk),
"name": str(provider), "name": str(provider),
"action": DeleteAction.SET_NULL.value, "action": DeleteAction.SET_NULL.name,
} }
], ],
) )

View File

@ -13,10 +13,11 @@ from rest_framework.fields import CharField, IntegerField
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from authentik.core.api.used_by import UsedByMixin from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer, PassiveSerializer from authentik.core.api.utils import PassiveSerializer
from authentik.core.models import User, UserTypes from authentik.core.models import User, UserTypes
from authentik.enterprise.license import LicenseKey, LicenseSummarySerializer from authentik.enterprise.license import LicenseKey, LicenseSummarySerializer
from authentik.enterprise.models import License from authentik.enterprise.models import License

View File

@ -1,13 +1,12 @@
"""GoogleWorkspaceProviderGroup API Views""" """GoogleWorkspaceProviderGroup API Views"""
from rest_framework import mixins from rest_framework import mixins
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import GenericViewSet from rest_framework.viewsets import GenericViewSet
from authentik.core.api.used_by import UsedByMixin from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.users import UserGroupSerializer from authentik.core.api.users import UserGroupSerializer
from authentik.core.api.utils import ModelSerializer
from authentik.enterprise.providers.google_workspace.models import GoogleWorkspaceProviderGroup from authentik.enterprise.providers.google_workspace.models import GoogleWorkspaceProviderGroup
from authentik.lib.sync.outgoing.api import OutgoingSyncConnectionCreateMixin
class GoogleWorkspaceProviderGroupSerializer(ModelSerializer): class GoogleWorkspaceProviderGroupSerializer(ModelSerializer):
@ -20,7 +19,6 @@ class GoogleWorkspaceProviderGroupSerializer(ModelSerializer):
model = GoogleWorkspaceProviderGroup model = GoogleWorkspaceProviderGroup
fields = [ fields = [
"id", "id",
"google_id",
"group", "group",
"group_obj", "group_obj",
"provider", "provider",
@ -31,7 +29,6 @@ class GoogleWorkspaceProviderGroupSerializer(ModelSerializer):
class GoogleWorkspaceProviderGroupViewSet( class GoogleWorkspaceProviderGroupViewSet(
mixins.CreateModelMixin, mixins.CreateModelMixin,
OutgoingSyncConnectionCreateMixin,
mixins.RetrieveModelMixin, mixins.RetrieveModelMixin,
mixins.DestroyModelMixin, mixins.DestroyModelMixin,
UsedByMixin, UsedByMixin,

View File

@ -1,13 +1,12 @@
"""GoogleWorkspaceProviderUser API Views""" """GoogleWorkspaceProviderUser API Views"""
from rest_framework import mixins from rest_framework import mixins
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import GenericViewSet from rest_framework.viewsets import GenericViewSet
from authentik.core.api.groups import GroupMemberSerializer from authentik.core.api.groups import GroupMemberSerializer
from authentik.core.api.used_by import UsedByMixin from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer
from authentik.enterprise.providers.google_workspace.models import GoogleWorkspaceProviderUser from authentik.enterprise.providers.google_workspace.models import GoogleWorkspaceProviderUser
from authentik.lib.sync.outgoing.api import OutgoingSyncConnectionCreateMixin
class GoogleWorkspaceProviderUserSerializer(ModelSerializer): class GoogleWorkspaceProviderUserSerializer(ModelSerializer):
@ -20,7 +19,6 @@ class GoogleWorkspaceProviderUserSerializer(ModelSerializer):
model = GoogleWorkspaceProviderUser model = GoogleWorkspaceProviderUser
fields = [ fields = [
"id", "id",
"google_id",
"user", "user",
"user_obj", "user_obj",
"provider", "provider",
@ -31,7 +29,6 @@ class GoogleWorkspaceProviderUserSerializer(ModelSerializer):
class GoogleWorkspaceProviderUserViewSet( class GoogleWorkspaceProviderUserViewSet(
mixins.CreateModelMixin, mixins.CreateModelMixin,
OutgoingSyncConnectionCreateMixin,
mixins.RetrieveModelMixin, mixins.RetrieveModelMixin,
mixins.DestroyModelMixin, mixins.DestroyModelMixin,
UsedByMixin, UsedByMixin,

View File

@ -92,14 +92,12 @@ class GoogleWorkspaceGroupClient(
google_group = self.to_schema(group, connection) google_group = self.to_schema(group, connection)
self.check_email_valid(google_group["email"]) self.check_email_valid(google_group["email"])
try: try:
response = self._request( return self._request(
self.directory_service.groups().update( self.directory_service.groups().update(
groupKey=connection.google_id, groupKey=connection.google_id,
body=google_group, body=google_group,
) )
) )
connection.attributes = response
connection.save()
except NotFoundSyncException: except NotFoundSyncException:
# Resource missing is handled by self.write, which will re-create the group # Resource missing is handled by self.write, which will re-create the group
raise raise
@ -214,7 +212,3 @@ class GoogleWorkspaceGroupClient(
google_id=google_id, google_id=google_id,
attributes=group, attributes=group,
) )
def update_single_attribute(self, connection: GoogleWorkspaceProviderUser):
group = self.directory_service.groups().get(connection.google_id)
connection.attributes = group

View File

@ -88,11 +88,9 @@ class GoogleWorkspaceUserClient(GoogleWorkspaceSyncClient[User, GoogleWorkspaceP
self.check_email_valid( self.check_email_valid(
google_user["primaryEmail"], *[x["address"] for x in google_user.get("emails", [])] google_user["primaryEmail"], *[x["address"] for x in google_user.get("emails", [])]
) )
response = self._request( self._request(
self.directory_service.users().update(userKey=connection.google_id, body=google_user) self.directory_service.users().update(userKey=connection.google_id, body=google_user)
) )
connection.attributes = response
connection.save()
def discover(self): def discover(self):
"""Iterate through all users and connect them with authentik users if possible""" """Iterate through all users and connect them with authentik users if possible"""
@ -119,7 +117,3 @@ class GoogleWorkspaceUserClient(GoogleWorkspaceSyncClient[User, GoogleWorkspaceP
google_id=email, google_id=email,
attributes=user, attributes=user,
) )
def update_single_attribute(self, connection: GoogleWorkspaceProviderUser):
user = self.directory_service.users().get(connection.google_id)
connection.attributes = user

View File

@ -31,58 +31,6 @@ def default_scopes() -> list[str]:
] ]
class GoogleWorkspaceProviderUser(SerializerModel):
"""Mapping of a user and provider to a Google user ID"""
id = models.UUIDField(primary_key=True, editable=False, default=uuid4)
google_id = models.TextField()
user = models.ForeignKey(User, on_delete=models.CASCADE)
provider = models.ForeignKey("GoogleWorkspaceProvider", on_delete=models.CASCADE)
attributes = models.JSONField(default=dict)
@property
def serializer(self) -> type[Serializer]:
from authentik.enterprise.providers.google_workspace.api.users import (
GoogleWorkspaceProviderUserSerializer,
)
return GoogleWorkspaceProviderUserSerializer
class Meta:
verbose_name = _("Google Workspace Provider User")
verbose_name_plural = _("Google Workspace Provider Users")
unique_together = (("google_id", "user", "provider"),)
def __str__(self) -> str:
return f"Google Workspace Provider User {self.user_id} to {self.provider_id}"
class GoogleWorkspaceProviderGroup(SerializerModel):
"""Mapping of a group and provider to a Google group ID"""
id = models.UUIDField(primary_key=True, editable=False, default=uuid4)
google_id = models.TextField()
group = models.ForeignKey(Group, on_delete=models.CASCADE)
provider = models.ForeignKey("GoogleWorkspaceProvider", on_delete=models.CASCADE)
attributes = models.JSONField(default=dict)
@property
def serializer(self) -> type[Serializer]:
from authentik.enterprise.providers.google_workspace.api.groups import (
GoogleWorkspaceProviderGroupSerializer,
)
return GoogleWorkspaceProviderGroupSerializer
class Meta:
verbose_name = _("Google Workspace Provider Group")
verbose_name_plural = _("Google Workspace Provider Groups")
unique_together = (("google_id", "group", "provider"),)
def __str__(self) -> str:
return f"Google Workspace Provider Group {self.group_id} to {self.provider_id}"
class GoogleWorkspaceProvider(OutgoingSyncProvider, BackchannelProvider): class GoogleWorkspaceProvider(OutgoingSyncProvider, BackchannelProvider):
"""Sync users from authentik into Google Workspace.""" """Sync users from authentik into Google Workspace."""
@ -111,16 +59,15 @@ class GoogleWorkspaceProvider(OutgoingSyncProvider, BackchannelProvider):
) )
def client_for_model( def client_for_model(
self, self, model: type[User | Group]
model: type[User | Group | GoogleWorkspaceProviderUser | GoogleWorkspaceProviderGroup],
) -> BaseOutgoingSyncClient[User | Group, Any, Any, Self]: ) -> BaseOutgoingSyncClient[User | Group, Any, Any, Self]:
if issubclass(model, User | GoogleWorkspaceProviderUser): if issubclass(model, User):
from authentik.enterprise.providers.google_workspace.clients.users import ( from authentik.enterprise.providers.google_workspace.clients.users import (
GoogleWorkspaceUserClient, GoogleWorkspaceUserClient,
) )
return GoogleWorkspaceUserClient(self) return GoogleWorkspaceUserClient(self)
if issubclass(model, Group | GoogleWorkspaceProviderGroup): if issubclass(model, Group):
from authentik.enterprise.providers.google_workspace.clients.groups import ( from authentik.enterprise.providers.google_workspace.clients.groups import (
GoogleWorkspaceGroupClient, GoogleWorkspaceGroupClient,
) )
@ -197,3 +144,55 @@ class GoogleWorkspaceProviderMapping(PropertyMapping):
class Meta: class Meta:
verbose_name = _("Google Workspace Provider Mapping") verbose_name = _("Google Workspace Provider Mapping")
verbose_name_plural = _("Google Workspace Provider Mappings") verbose_name_plural = _("Google Workspace Provider Mappings")
class GoogleWorkspaceProviderUser(SerializerModel):
"""Mapping of a user and provider to a Google user ID"""
id = models.UUIDField(primary_key=True, editable=False, default=uuid4)
google_id = models.TextField()
user = models.ForeignKey(User, on_delete=models.CASCADE)
provider = models.ForeignKey(GoogleWorkspaceProvider, on_delete=models.CASCADE)
attributes = models.JSONField(default=dict)
@property
def serializer(self) -> type[Serializer]:
from authentik.enterprise.providers.google_workspace.api.users import (
GoogleWorkspaceProviderUserSerializer,
)
return GoogleWorkspaceProviderUserSerializer
class Meta:
verbose_name = _("Google Workspace Provider User")
verbose_name_plural = _("Google Workspace Provider Users")
unique_together = (("google_id", "user", "provider"),)
def __str__(self) -> str:
return f"Google Workspace Provider User {self.user_id} to {self.provider_id}"
class GoogleWorkspaceProviderGroup(SerializerModel):
"""Mapping of a group and provider to a Google group ID"""
id = models.UUIDField(primary_key=True, editable=False, default=uuid4)
google_id = models.TextField()
group = models.ForeignKey(Group, on_delete=models.CASCADE)
provider = models.ForeignKey(GoogleWorkspaceProvider, on_delete=models.CASCADE)
attributes = models.JSONField(default=dict)
@property
def serializer(self) -> type[Serializer]:
from authentik.enterprise.providers.google_workspace.api.groups import (
GoogleWorkspaceProviderGroupSerializer,
)
return GoogleWorkspaceProviderGroupSerializer
class Meta:
verbose_name = _("Google Workspace Provider Group")
verbose_name_plural = _("Google Workspace Provider Groups")
unique_together = (("google_id", "group", "provider"),)
def __str__(self) -> str:
return f"Google Workspace Provider Group {self.group_id} to {self.provider_id}"

View File

@ -1,13 +1,12 @@
"""MicrosoftEntraProviderGroup API Views""" """MicrosoftEntraProviderGroup API Views"""
from rest_framework import mixins from rest_framework import mixins
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import GenericViewSet from rest_framework.viewsets import GenericViewSet
from authentik.core.api.used_by import UsedByMixin from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.users import UserGroupSerializer from authentik.core.api.users import UserGroupSerializer
from authentik.core.api.utils import ModelSerializer
from authentik.enterprise.providers.microsoft_entra.models import MicrosoftEntraProviderGroup from authentik.enterprise.providers.microsoft_entra.models import MicrosoftEntraProviderGroup
from authentik.lib.sync.outgoing.api import OutgoingSyncConnectionCreateMixin
class MicrosoftEntraProviderGroupSerializer(ModelSerializer): class MicrosoftEntraProviderGroupSerializer(ModelSerializer):
@ -20,7 +19,6 @@ class MicrosoftEntraProviderGroupSerializer(ModelSerializer):
model = MicrosoftEntraProviderGroup model = MicrosoftEntraProviderGroup
fields = [ fields = [
"id", "id",
"microsoft_id",
"group", "group",
"group_obj", "group_obj",
"provider", "provider",
@ -31,7 +29,6 @@ class MicrosoftEntraProviderGroupSerializer(ModelSerializer):
class MicrosoftEntraProviderGroupViewSet( class MicrosoftEntraProviderGroupViewSet(
mixins.CreateModelMixin, mixins.CreateModelMixin,
OutgoingSyncConnectionCreateMixin,
mixins.RetrieveModelMixin, mixins.RetrieveModelMixin,
mixins.DestroyModelMixin, mixins.DestroyModelMixin,
UsedByMixin, UsedByMixin,

View File

@ -1,13 +1,12 @@
"""MicrosoftEntraProviderUser API Views""" """MicrosoftEntraProviderUser API Views"""
from rest_framework import mixins from rest_framework import mixins
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import GenericViewSet from rest_framework.viewsets import GenericViewSet
from authentik.core.api.groups import GroupMemberSerializer from authentik.core.api.groups import GroupMemberSerializer
from authentik.core.api.used_by import UsedByMixin from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer
from authentik.enterprise.providers.microsoft_entra.models import MicrosoftEntraProviderUser from authentik.enterprise.providers.microsoft_entra.models import MicrosoftEntraProviderUser
from authentik.lib.sync.outgoing.api import OutgoingSyncConnectionCreateMixin
class MicrosoftEntraProviderUserSerializer(ModelSerializer): class MicrosoftEntraProviderUserSerializer(ModelSerializer):
@ -20,7 +19,6 @@ class MicrosoftEntraProviderUserSerializer(ModelSerializer):
model = MicrosoftEntraProviderUser model = MicrosoftEntraProviderUser
fields = [ fields = [
"id", "id",
"microsoft_id",
"user", "user",
"user_obj", "user_obj",
"provider", "provider",
@ -30,7 +28,6 @@ class MicrosoftEntraProviderUserSerializer(ModelSerializer):
class MicrosoftEntraProviderUserViewSet( class MicrosoftEntraProviderUserViewSet(
OutgoingSyncConnectionCreateMixin,
mixins.CreateModelMixin, mixins.CreateModelMixin,
mixins.RetrieveModelMixin, mixins.RetrieveModelMixin,
mixins.DestroyModelMixin, mixins.DestroyModelMixin,

View File

@ -23,7 +23,6 @@ from msgraph.graph_service_client import GraphServiceClient
from msgraph_core import GraphClientFactory from msgraph_core import GraphClientFactory
from authentik.enterprise.providers.microsoft_entra.models import MicrosoftEntraProvider from authentik.enterprise.providers.microsoft_entra.models import MicrosoftEntraProvider
from authentik.events.utils import sanitize_item
from authentik.lib.sync.outgoing import HTTP_CONFLICT from authentik.lib.sync.outgoing import HTTP_CONFLICT
from authentik.lib.sync.outgoing.base import BaseOutgoingSyncClient from authentik.lib.sync.outgoing.base import BaseOutgoingSyncClient
from authentik.lib.sync.outgoing.exceptions import ( from authentik.lib.sync.outgoing.exceptions import (
@ -107,4 +106,4 @@ class MicrosoftEntraSyncClient[TModel: Model, TConnection: Model, TSchema: dict]
we can't JSON serialize""" we can't JSON serialize"""
raw_data = asdict(entity) raw_data = asdict(entity)
raw_data.pop("backing_store", None) raw_data.pop("backing_store", None)
return sanitize_item(raw_data) return raw_data

View File

@ -1,4 +1,3 @@
from deepmerge import always_merger
from django.db import transaction from django.db import transaction
from msgraph.generated.groups.groups_request_builder import GroupsRequestBuilder from msgraph.generated.groups.groups_request_builder import GroupsRequestBuilder
from msgraph.generated.models.group import Group as MSGroup from msgraph.generated.models.group import Group as MSGroup
@ -105,12 +104,9 @@ class MicrosoftEntraGroupClient(
microsoft_group = self.to_schema(group, connection) microsoft_group = self.to_schema(group, connection)
microsoft_group.id = connection.microsoft_id microsoft_group.id = connection.microsoft_id
try: try:
response = self._request( return self._request(
self.client.groups.by_group_id(connection.microsoft_id).patch(microsoft_group) self.client.groups.by_group_id(connection.microsoft_id).patch(microsoft_group)
) )
if response:
always_merger.merge(connection.attributes, self.entity_as_dict(response))
connection.save()
except NotFoundSyncException: except NotFoundSyncException:
# Resource missing is handled by self.write, which will re-create the group # Resource missing is handled by self.write, which will re-create the group
raise raise
@ -226,7 +222,3 @@ class MicrosoftEntraGroupClient(
microsoft_id=group.id, microsoft_id=group.id,
attributes=self.entity_as_dict(group), attributes=self.entity_as_dict(group),
) )
def update_single_attribute(self, connection: MicrosoftEntraProviderGroup):
data = self._request(self.client.groups.by_group_id(connection.microsoft_id).get())
connection.attributes = self.entity_as_dict(data)

View File

@ -1,4 +1,3 @@
from deepmerge import always_merger
from django.db import transaction from django.db import transaction
from msgraph.generated.models.user import User as MSUser from msgraph.generated.models.user import User as MSUser
from msgraph.generated.users.users_request_builder import UsersRequestBuilder from msgraph.generated.users.users_request_builder import UsersRequestBuilder
@ -66,26 +65,6 @@ class MicrosoftEntraUserClient(MicrosoftEntraSyncClient[User, MicrosoftEntraProv
microsoft_user.delete() microsoft_user.delete()
return response return response
def get_select_fields(self) -> list[str]:
"""All fields that should be selected when we fetch user data."""
# TODO: Make this customizable in the future
return [
# Default fields
"businessPhones",
"displayName",
"givenName",
"jobTitle",
"mail",
"mobilePhone",
"officeLocation",
"preferredLanguage",
"surname",
"userPrincipalName",
"id",
# Required for logging into M365 using authentik
"onPremisesImmutableId",
]
def create(self, user: User): def create(self, user: User):
"""Create user from scratch and create a connection object""" """Create user from scratch and create a connection object"""
microsoft_user = self.to_schema(user, None) microsoft_user = self.to_schema(user, None)
@ -95,12 +74,12 @@ class MicrosoftEntraUserClient(MicrosoftEntraSyncClient[User, MicrosoftEntraProv
response = self._request(self.client.users.post(microsoft_user)) response = self._request(self.client.users.post(microsoft_user))
except ObjectExistsSyncException: except ObjectExistsSyncException:
# user already exists in microsoft entra, so we can connect them manually # user already exists in microsoft entra, so we can connect them manually
query_params = UsersRequestBuilder.UsersRequestBuilderGetQueryParameters()(
filter=f"mail eq '{microsoft_user.mail}'",
)
request_configuration = ( request_configuration = (
UsersRequestBuilder.UsersRequestBuilderGetRequestConfiguration( UsersRequestBuilder.UsersRequestBuilderGetRequestConfiguration(
query_parameters=UsersRequestBuilder.UsersRequestBuilderGetQueryParameters( query_parameters=query_params,
filter=f"mail eq '{microsoft_user.mail}'",
select=self.get_select_fields(),
),
) )
) )
user_data = self._request(self.client.users.get(request_configuration)) user_data = self._request(self.client.users.get(request_configuration))
@ -119,6 +98,7 @@ class MicrosoftEntraUserClient(MicrosoftEntraSyncClient[User, MicrosoftEntraProv
except TransientSyncException as exc: except TransientSyncException as exc:
raise exc raise exc
else: else:
print(self.entity_as_dict(response))
return MicrosoftEntraProviderUser.objects.create( return MicrosoftEntraProviderUser.objects.create(
provider=self.provider, provider=self.provider,
user=user, user=user,
@ -130,21 +110,11 @@ class MicrosoftEntraUserClient(MicrosoftEntraSyncClient[User, MicrosoftEntraProv
"""Update existing user""" """Update existing user"""
microsoft_user = self.to_schema(user, connection) microsoft_user = self.to_schema(user, connection)
self.check_email_valid(microsoft_user.user_principal_name) self.check_email_valid(microsoft_user.user_principal_name)
response = self._request( self._request(self.client.users.by_user_id(connection.microsoft_id).patch(microsoft_user))
self.client.users.by_user_id(connection.microsoft_id).patch(microsoft_user)
)
if response:
always_merger.merge(connection.attributes, self.entity_as_dict(response))
connection.save()
def discover(self): def discover(self):
"""Iterate through all users and connect them with authentik users if possible""" """Iterate through all users and connect them with authentik users if possible"""
request_configuration = UsersRequestBuilder.UsersRequestBuilderGetRequestConfiguration( users = self._request(self.client.users.get())
query_parameters=UsersRequestBuilder.UsersRequestBuilderGetQueryParameters(
select=self.get_select_fields(),
),
)
users = self._request(self.client.users.get(request_configuration))
next_link = True next_link = True
while next_link: while next_link:
for user in users.value: for user in users.value:
@ -165,14 +135,3 @@ class MicrosoftEntraUserClient(MicrosoftEntraSyncClient[User, MicrosoftEntraProv
microsoft_id=user.id, microsoft_id=user.id,
attributes=self.entity_as_dict(user), attributes=self.entity_as_dict(user),
) )
def update_single_attribute(self, connection: MicrosoftEntraProviderUser):
request_configuration = UsersRequestBuilder.UsersRequestBuilderGetRequestConfiguration(
query_parameters=UsersRequestBuilder.UsersRequestBuilderGetQueryParameters(
select=self.get_select_fields(),
),
)
data = self._request(
self.client.users.by_user_id(connection.microsoft_id).get(request_configuration)
)
connection.attributes = self.entity_as_dict(data)

View File

@ -22,58 +22,6 @@ from authentik.lib.sync.outgoing.base import BaseOutgoingSyncClient
from authentik.lib.sync.outgoing.models import OutgoingSyncDeleteAction, OutgoingSyncProvider from authentik.lib.sync.outgoing.models import OutgoingSyncDeleteAction, OutgoingSyncProvider
class MicrosoftEntraProviderUser(SerializerModel):
"""Mapping of a user and provider to a Microsoft user ID"""
id = models.UUIDField(primary_key=True, editable=False, default=uuid4)
microsoft_id = models.TextField()
user = models.ForeignKey(User, on_delete=models.CASCADE)
provider = models.ForeignKey("MicrosoftEntraProvider", on_delete=models.CASCADE)
attributes = models.JSONField(default=dict)
@property
def serializer(self) -> type[Serializer]:
from authentik.enterprise.providers.microsoft_entra.api.users import (
MicrosoftEntraProviderUserSerializer,
)
return MicrosoftEntraProviderUserSerializer
class Meta:
verbose_name = _("Microsoft Entra Provider User")
verbose_name_plural = _("Microsoft Entra Provider User")
unique_together = (("microsoft_id", "user", "provider"),)
def __str__(self) -> str:
return f"Microsoft Entra Provider User {self.user_id} to {self.provider_id}"
class MicrosoftEntraProviderGroup(SerializerModel):
"""Mapping of a group and provider to a Microsoft group ID"""
id = models.UUIDField(primary_key=True, editable=False, default=uuid4)
microsoft_id = models.TextField()
group = models.ForeignKey(Group, on_delete=models.CASCADE)
provider = models.ForeignKey("MicrosoftEntraProvider", on_delete=models.CASCADE)
attributes = models.JSONField(default=dict)
@property
def serializer(self) -> type[Serializer]:
from authentik.enterprise.providers.microsoft_entra.api.groups import (
MicrosoftEntraProviderGroupSerializer,
)
return MicrosoftEntraProviderGroupSerializer
class Meta:
verbose_name = _("Microsoft Entra Provider Group")
verbose_name_plural = _("Microsoft Entra Provider Groups")
unique_together = (("microsoft_id", "group", "provider"),)
def __str__(self) -> str:
return f"Microsoft Entra Provider Group {self.group_id} to {self.provider_id}"
class MicrosoftEntraProvider(OutgoingSyncProvider, BackchannelProvider): class MicrosoftEntraProvider(OutgoingSyncProvider, BackchannelProvider):
"""Sync users from authentik into Microsoft Entra.""" """Sync users from authentik into Microsoft Entra."""
@ -100,16 +48,15 @@ class MicrosoftEntraProvider(OutgoingSyncProvider, BackchannelProvider):
) )
def client_for_model( def client_for_model(
self, self, model: type[User | Group]
model: type[User | Group | MicrosoftEntraProviderUser | MicrosoftEntraProviderGroup],
) -> BaseOutgoingSyncClient[User | Group, Any, Any, Self]: ) -> BaseOutgoingSyncClient[User | Group, Any, Any, Self]:
if issubclass(model, User | MicrosoftEntraProviderUser): if issubclass(model, User):
from authentik.enterprise.providers.microsoft_entra.clients.users import ( from authentik.enterprise.providers.microsoft_entra.clients.users import (
MicrosoftEntraUserClient, MicrosoftEntraUserClient,
) )
return MicrosoftEntraUserClient(self) return MicrosoftEntraUserClient(self)
if issubclass(model, Group | MicrosoftEntraProviderGroup): if issubclass(model, Group):
from authentik.enterprise.providers.microsoft_entra.clients.groups import ( from authentik.enterprise.providers.microsoft_entra.clients.groups import (
MicrosoftEntraGroupClient, MicrosoftEntraGroupClient,
) )
@ -186,3 +133,55 @@ class MicrosoftEntraProviderMapping(PropertyMapping):
class Meta: class Meta:
verbose_name = _("Microsoft Entra Provider Mapping") verbose_name = _("Microsoft Entra Provider Mapping")
verbose_name_plural = _("Microsoft Entra Provider Mappings") verbose_name_plural = _("Microsoft Entra Provider Mappings")
class MicrosoftEntraProviderUser(SerializerModel):
"""Mapping of a user and provider to a Microsoft user ID"""
id = models.UUIDField(primary_key=True, editable=False, default=uuid4)
microsoft_id = models.TextField()
user = models.ForeignKey(User, on_delete=models.CASCADE)
provider = models.ForeignKey(MicrosoftEntraProvider, on_delete=models.CASCADE)
attributes = models.JSONField(default=dict)
@property
def serializer(self) -> type[Serializer]:
from authentik.enterprise.providers.microsoft_entra.api.users import (
MicrosoftEntraProviderUserSerializer,
)
return MicrosoftEntraProviderUserSerializer
class Meta:
verbose_name = _("Microsoft Entra Provider User")
verbose_name_plural = _("Microsoft Entra Provider User")
unique_together = (("microsoft_id", "user", "provider"),)
def __str__(self) -> str:
return f"Microsoft Entra Provider User {self.user_id} to {self.provider_id}"
class MicrosoftEntraProviderGroup(SerializerModel):
"""Mapping of a group and provider to a Microsoft group ID"""
id = models.UUIDField(primary_key=True, editable=False, default=uuid4)
microsoft_id = models.TextField()
group = models.ForeignKey(Group, on_delete=models.CASCADE)
provider = models.ForeignKey(MicrosoftEntraProvider, on_delete=models.CASCADE)
attributes = models.JSONField(default=dict)
@property
def serializer(self) -> type[Serializer]:
from authentik.enterprise.providers.microsoft_entra.api.groups import (
MicrosoftEntraProviderGroupSerializer,
)
return MicrosoftEntraProviderGroupSerializer
class Meta:
verbose_name = _("Microsoft Entra Provider Group")
verbose_name_plural = _("Microsoft Entra Provider Groups")
unique_together = (("microsoft_id", "group", "provider"),)
def __str__(self) -> str:
return f"Microsoft Entra Provider Group {self.group_id} to {self.provider_id}"

View File

@ -3,18 +3,16 @@
from unittest.mock import AsyncMock, MagicMock, patch from unittest.mock import AsyncMock, MagicMock, patch
from azure.identity.aio import ClientSecretCredential from azure.identity.aio import ClientSecretCredential
from django.urls import reverse from django.test import TestCase
from msgraph.generated.models.group_collection_response import GroupCollectionResponse from msgraph.generated.models.group_collection_response import GroupCollectionResponse
from msgraph.generated.models.organization import Organization from msgraph.generated.models.organization import Organization
from msgraph.generated.models.organization_collection_response import OrganizationCollectionResponse from msgraph.generated.models.organization_collection_response import OrganizationCollectionResponse
from msgraph.generated.models.user import User as MSUser from msgraph.generated.models.user import User as MSUser
from msgraph.generated.models.user_collection_response import UserCollectionResponse from msgraph.generated.models.user_collection_response import UserCollectionResponse
from msgraph.generated.models.verified_domain import VerifiedDomain from msgraph.generated.models.verified_domain import VerifiedDomain
from rest_framework.test import APITestCase
from authentik.blueprints.tests import apply_blueprint from authentik.blueprints.tests import apply_blueprint
from authentik.core.models import Application, Group, User from authentik.core.models import Application, Group, User
from authentik.core.tests.utils import create_test_admin_user
from authentik.enterprise.providers.microsoft_entra.models import ( from authentik.enterprise.providers.microsoft_entra.models import (
MicrosoftEntraProvider, MicrosoftEntraProvider,
MicrosoftEntraProviderMapping, MicrosoftEntraProviderMapping,
@ -27,12 +25,11 @@ from authentik.lib.sync.outgoing.models import OutgoingSyncDeleteAction
from authentik.tenants.models import Tenant from authentik.tenants.models import Tenant
class MicrosoftEntraUserTests(APITestCase): class MicrosoftEntraUserTests(TestCase):
"""Microsoft Entra User tests""" """Microsoft Entra User tests"""
@apply_blueprint("system/providers-microsoft-entra.yaml") @apply_blueprint("system/providers-microsoft-entra.yaml")
def setUp(self) -> None: def setUp(self) -> None:
# Delete all users and groups as the mocked HTTP responses only return one ID # Delete all users and groups as the mocked HTTP responses only return one ID
# which will cause errors with multiple users # which will cause errors with multiple users
Tenant.objects.update(avatars="none") Tenant.objects.update(avatars="none")
@ -374,45 +371,3 @@ class MicrosoftEntraUserTests(APITestCase):
) )
self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists()) self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists())
user_list.assert_called_once() user_list.assert_called_once()
def test_connect_manual(self):
"""test manual user connection"""
uid = generate_id()
self.app.backchannel_providers.remove(self.provider)
admin = create_test_admin_user()
different_user = User.objects.create(
username=uid,
email=f"{uid}@goauthentik.io",
)
self.app.backchannel_providers.add(self.provider)
with (
patch(
"authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProvider.microsoft_credentials",
MagicMock(return_value={"credentials": self.creds}),
),
patch(
"msgraph.generated.organization.organization_request_builder.OrganizationRequestBuilder.get",
AsyncMock(
return_value=OrganizationCollectionResponse(
value=[
Organization(verified_domains=[VerifiedDomain(name="goauthentik.io")])
]
)
),
),
patch(
"authentik.enterprise.providers.microsoft_entra.clients.users.MicrosoftEntraUserClient.update_single_attribute",
MagicMock(),
) as user_get,
):
self.client.force_login(admin)
response = self.client.post(
reverse("authentik_api:microsoftentraprovideruser-list"),
data={
"microsoft_id": generate_id(),
"user": different_user.pk,
"provider": self.provider.pk,
},
)
self.assertEqual(response.status_code, 201)
user_get.assert_called_once()

View File

@ -3,12 +3,12 @@
from django_filters.rest_framework.backends import DjangoFilterBackend from django_filters.rest_framework.backends import DjangoFilterBackend
from rest_framework import mixins from rest_framework import mixins
from rest_framework.filters import OrderingFilter, SearchFilter from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import GenericViewSet from rest_framework.viewsets import GenericViewSet
from authentik.api.authorization import OwnerFilter, OwnerSuperuserPermissions from authentik.api.authorization import OwnerFilter, OwnerSuperuserPermissions
from authentik.core.api.groups import GroupMemberSerializer from authentik.core.api.groups import GroupMemberSerializer
from authentik.core.api.used_by import UsedByMixin from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer
from authentik.enterprise.api import EnterpriseRequiredMixin from authentik.enterprise.api import EnterpriseRequiredMixin
from authentik.enterprise.providers.rac.api.endpoints import EndpointSerializer from authentik.enterprise.providers.rac.api.endpoints import EndpointSerializer
from authentik.enterprise.providers.rac.api.providers import RACProviderSerializer from authentik.enterprise.providers.rac.api.providers import RACProviderSerializer

View File

@ -8,11 +8,11 @@ from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_sche
from rest_framework.fields import SerializerMethodField from rest_framework.fields import SerializerMethodField
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from authentik.core.api.used_by import UsedByMixin from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer
from authentik.core.models import Provider from authentik.core.models import Provider
from authentik.enterprise.api import EnterpriseRequiredMixin from authentik.enterprise.api import EnterpriseRequiredMixin
from authentik.enterprise.providers.rac.api.providers import RACProviderSerializer from authentik.enterprise.providers.rac.api.providers import RACProviderSerializer

View File

@ -1,9 +1,9 @@
{% extends "base/skeleton.html" %} {% extends "base/skeleton.html" %}
{% load authentik_core %} {% load static %}
{% block head %} {% block head %}
{% versioned_script "dist/enterprise/rac/index-%v.js" %} <script src="{% static 'dist/enterprise/rac/index.js' %}?version={{ version }}" type="module"></script>
<meta name="theme-color" content="#18191a" media="(prefers-color-scheme: dark)"> <meta name="theme-color" content="#18191a" media="(prefers-color-scheme: dark)">
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)"> <meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">
<link rel="icon" href="{{ tenant.branding_favicon }}"> <link rel="icon" href="{{ tenant.branding_favicon }}">

View File

@ -15,11 +15,12 @@ from rest_framework.decorators import action
from rest_framework.fields import DictField, IntegerField from rest_framework.fields import DictField, IntegerField
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from authentik.admin.api.metrics import CoordinateSerializer from authentik.admin.api.metrics import CoordinateSerializer
from authentik.core.api.object_types import TypeCreateSerializer from authentik.core.api.object_types import TypeCreateSerializer
from authentik.core.api.utils import ModelSerializer, PassiveSerializer from authentik.core.api.utils import PassiveSerializer
from authentik.events.models import Event, EventAction from authentik.events.models import Event, EventAction

View File

@ -1,9 +1,9 @@
"""NotificationWebhookMapping API Views""" """NotificationWebhookMapping API Views"""
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from authentik.core.api.used_by import UsedByMixin from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer
from authentik.events.models import NotificationWebhookMapping from authentik.events.models import NotificationWebhookMapping

View File

@ -1,10 +1,10 @@
"""NotificationRule API Views""" """NotificationRule API Views"""
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from authentik.core.api.groups import GroupSerializer from authentik.core.api.groups import GroupSerializer
from authentik.core.api.used_by import UsedByMixin from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer
from authentik.events.models import NotificationRule from authentik.events.models import NotificationRule

View File

@ -9,10 +9,11 @@ from rest_framework.exceptions import ValidationError
from rest_framework.fields import CharField, ListField, SerializerMethodField from rest_framework.fields import CharField, ListField, SerializerMethodField
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from authentik.core.api.used_by import UsedByMixin from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer, PassiveSerializer from authentik.core.api.utils import PassiveSerializer
from authentik.events.models import ( from authentik.events.models import (
Event, Event,
Notification, Notification,

View File

@ -9,11 +9,11 @@ from rest_framework.fields import ReadOnlyField
from rest_framework.filters import OrderingFilter, SearchFilter from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import GenericViewSet from rest_framework.viewsets import GenericViewSet
from authentik.api.authorization import OwnerFilter, OwnerPermissions from authentik.api.authorization import OwnerFilter, OwnerPermissions
from authentik.core.api.used_by import UsedByMixin from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer
from authentik.events.api.events import EventSerializer from authentik.events.api.events import EventSerializer
from authentik.events.models import Notification from authentik.events.models import Notification

View File

@ -16,10 +16,10 @@ from rest_framework.fields import (
) )
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ReadOnlyModelViewSet from rest_framework.viewsets import ReadOnlyModelViewSet
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from authentik.core.api.utils import ModelSerializer
from authentik.events.logs import LogEventSerializer from authentik.events.logs import LogEventSerializer
from authentik.events.models import SystemTask, TaskStatus from authentik.events.models import SystemTask, TaskStatus
from authentik.rbac.decorators import permission_required from authentik.rbac.decorators import permission_required

View File

@ -75,10 +75,7 @@ def on_login_failed(
**kwargs, **kwargs,
): ):
"""Failed Login, authentik custom event""" """Failed Login, authentik custom event"""
user = User.objects.filter(username=credentials.get("username")).first() Event.new(EventAction.LOGIN_FAILED, **credentials, stage=stage, **kwargs).from_http(request)
Event.new(EventAction.LOGIN_FAILED, **credentials, stage=stage, **kwargs).from_http(
request, user
)
@receiver(invitation_used) @receiver(invitation_used)

View File

@ -3,10 +3,10 @@
from typing import Any from typing import Any
from rest_framework.exceptions import ValidationError from rest_framework.exceptions import ValidationError
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from authentik.core.api.used_by import UsedByMixin from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer
from authentik.flows.api.stages import StageSerializer from authentik.flows.api.stages import StageSerializer
from authentik.flows.models import FlowStageBinding from authentik.flows.models import FlowStageBinding

View File

@ -7,22 +7,18 @@ from django.utils.translation import gettext as _
from drf_spectacular.types import OpenApiTypes from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiResponse, extend_schema from drf_spectacular.utils import OpenApiResponse, extend_schema
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.fields import BooleanField, CharField, ReadOnlyField, SerializerMethodField from rest_framework.fields import BooleanField, CharField, ReadOnlyField
from rest_framework.parsers import MultiPartParser from rest_framework.parsers import MultiPartParser
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer, SerializerMethodField
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from authentik.blueprints.v1.exporter import FlowExporter from authentik.blueprints.v1.exporter import FlowExporter
from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT, Importer from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT, Importer
from authentik.core.api.used_by import UsedByMixin from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ( from authentik.core.api.utils import CacheSerializer, LinkSerializer, PassiveSerializer
CacheSerializer,
LinkSerializer,
ModelSerializer,
PassiveSerializer,
)
from authentik.events.logs import LogEventSerializer from authentik.events.logs import LogEventSerializer
from authentik.flows.api.flows_diagram import FlowDiagram, FlowDiagramSerializer from authentik.flows.api.flows_diagram import FlowDiagram, FlowDiagramSerializer
from authentik.flows.exceptions import FlowNonApplicableException from authentik.flows.exceptions import FlowNonApplicableException

View File

@ -4,15 +4,15 @@ from django.urls.base import reverse
from drf_spectacular.utils import extend_schema from drf_spectacular.utils import extend_schema
from rest_framework import mixins from rest_framework import mixins
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.fields import SerializerMethodField
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer, SerializerMethodField
from rest_framework.viewsets import GenericViewSet from rest_framework.viewsets import GenericViewSet
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from authentik.core.api.object_types import TypesMixin from authentik.core.api.object_types import TypesMixin
from authentik.core.api.used_by import UsedByMixin from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import MetaNameSerializer, ModelSerializer from authentik.core.api.utils import MetaNameSerializer
from authentik.core.types import UserSettingSerializer from authentik.core.types import UserSettingSerializer
from authentik.flows.api.flows import FlowSetSerializer from authentik.flows.api.flows import FlowSetSerializer
from authentik.flows.models import ConfigurableStage, Stage from authentik.flows.models import ConfigurableStage, Stage

View File

@ -32,6 +32,14 @@ class FlowLayout(models.TextChoices):
SIDEBAR_RIGHT = "sidebar_right" SIDEBAR_RIGHT = "sidebar_right"
class ChallengeTypes(Enum):
"""Currently defined challenge types"""
NATIVE = "native"
SHELL = "shell"
REDIRECT = "redirect"
class ErrorDetailSerializer(PassiveSerializer): class ErrorDetailSerializer(PassiveSerializer):
"""Serializer for rest_framework's error messages""" """Serializer for rest_framework's error messages"""
@ -52,6 +60,9 @@ class Challenge(PassiveSerializer):
"""Challenge that gets sent to the client based on which stage """Challenge that gets sent to the client based on which stage
is currently active""" is currently active"""
type = ChoiceField(
choices=[(x.value, x.name) for x in ChallengeTypes],
)
flow_info = ContextualFlowInfo(required=False) flow_info = ContextualFlowInfo(required=False)
component = CharField(default="") component = CharField(default="")
@ -85,6 +96,7 @@ class FlowErrorChallenge(Challenge):
"""Challenge class when an unhandled error occurs during a stage. Normal users """Challenge class when an unhandled error occurs during a stage. Normal users
are shown an error message, superusers are shown a full stacktrace.""" are shown an error message, superusers are shown a full stacktrace."""
type = CharField(default=ChallengeTypes.NATIVE.value)
component = CharField(default="ak-stage-flow-error") component = CharField(default="ak-stage-flow-error")
request_id = CharField() request_id = CharField()

View File

@ -21,9 +21,7 @@ def set_oobe_flow_authentication(apps: Apps, schema_editor: BaseDatabaseSchemaEd
pass pass
if users.exists(): if users.exists():
Flow.objects.using(db_alias).filter(slug="initial-setup").update( Flow.objects.filter(slug="initial-setup").update(authentication="require_superuser")
authentication="require_superuser"
)
class Migration(migrations.Migration): class Migration(migrations.Migration):

View File

@ -18,6 +18,7 @@ from authentik.flows.challenge import (
AccessDeniedChallenge, AccessDeniedChallenge,
Challenge, Challenge,
ChallengeResponse, ChallengeResponse,
ChallengeTypes,
ContextualFlowInfo, ContextualFlowInfo,
HttpChallengeResponse, HttpChallengeResponse,
RedirectChallenge, RedirectChallenge,
@ -243,6 +244,7 @@ class AccessDeniedChallengeView(ChallengeStageView):
return AccessDeniedChallenge( return AccessDeniedChallenge(
data={ data={
"error_message": str(self.error_message or "Unknown error"), "error_message": str(self.error_message or "Unknown error"),
"type": ChallengeTypes.NATIVE.value,
"component": "ak-stage-access-denied", "component": "ak-stage-access-denied",
} }
) )
@ -262,6 +264,7 @@ class RedirectStage(ChallengeStageView):
) )
return RedirectChallenge( return RedirectChallenge(
data={ data={
"type": ChallengeTypes.REDIRECT.value,
"to": destination, "to": destination,
} }
) )

View File

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

View File

@ -8,6 +8,7 @@ from django.urls.base import reverse
from rest_framework.test import APITestCase from rest_framework.test import APITestCase
from authentik.core.models import User from authentik.core.models import User
from authentik.flows.challenge import ChallengeTypes
from authentik.flows.models import Flow from authentik.flows.models import Flow
@ -25,6 +26,7 @@ class FlowTestCase(APITestCase):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
raw_response = loads(response.content.decode()) raw_response = loads(response.content.decode())
self.assertIsNotNone(raw_response["component"]) self.assertIsNotNone(raw_response["component"])
self.assertIsNotNone(raw_response["type"])
if flow: if flow:
self.assertIn("flow_info", raw_response) self.assertIn("flow_info", raw_response)
self.assertEqual(raw_response["flow_info"]["background"], flow.background_url) self.assertEqual(raw_response["flow_info"]["background"], flow.background_url)
@ -44,4 +46,6 @@ class FlowTestCase(APITestCase):
def assertStageRedirects(self, response: HttpResponse, to: str) -> dict[str, Any]: def assertStageRedirects(self, response: HttpResponse, to: str) -> dict[str, Any]:
"""Wrapper around assertStageResponse that checks for a redirect""" """Wrapper around assertStageResponse that checks for a redirect"""
return self.assertStageResponse(response, component="xak-flow-redirect", to=to) return self.assertStageResponse(
response, component="xak-flow-redirect", to=to, type=ChallengeTypes.REDIRECT.value
)

View File

@ -2,7 +2,7 @@
from django.test import TestCase from django.test import TestCase
from authentik.flows.challenge import AutosubmitChallenge from authentik.flows.challenge import AutosubmitChallenge, ChallengeTypes
class TestChallenges(TestCase): class TestChallenges(TestCase):
@ -12,6 +12,7 @@ class TestChallenges(TestCase):
"""Test blank autosubmit""" """Test blank autosubmit"""
challenge = AutosubmitChallenge( challenge = AutosubmitChallenge(
data={ data={
"type": ChallengeTypes.NATIVE.value,
"url": "http://localhost", "url": "http://localhost",
"attrs": {}, "attrs": {},
} }
@ -20,6 +21,7 @@ class TestChallenges(TestCase):
# Test with an empty value # Test with an empty value
challenge = AutosubmitChallenge( challenge = AutosubmitChallenge(
data={ data={
"type": ChallengeTypes.NATIVE.value,
"url": "http://localhost", "url": "http://localhost",
"attrs": {"foo": ""}, "attrs": {"foo": ""},
} }

View File

@ -7,6 +7,7 @@ from django.urls.base import reverse
from rest_framework.test import APITestCase from rest_framework.test import APITestCase
from authentik.core.tests.utils import create_test_admin_user, create_test_flow from authentik.core.tests.utils import create_test_admin_user, create_test_flow
from authentik.flows.challenge import ChallengeTypes
from authentik.flows.models import FlowDesignation, FlowStageBinding, InvalidResponseAction from authentik.flows.models import FlowDesignation, FlowStageBinding, InvalidResponseAction
from authentik.stages.dummy.models import DummyStage from authentik.stages.dummy.models import DummyStage
from authentik.stages.identification.models import IdentificationStage, UserFields from authentik.stages.identification.models import IdentificationStage, UserFields
@ -53,6 +54,7 @@ class TestFlowInspector(APITestCase):
"layout": "stacked", "layout": "stacked",
}, },
"flow_designation": "authentication", "flow_designation": "authentication",
"type": ChallengeTypes.NATIVE.value,
"password_fields": False, "password_fields": False,
"primary_action": "Log in", "primary_action": "Log in",
"sources": [], "sources": [],

View File

@ -30,6 +30,7 @@ from authentik.flows.apps import HIST_FLOW_EXECUTION_STAGE_TIME
from authentik.flows.challenge import ( from authentik.flows.challenge import (
Challenge, Challenge,
ChallengeResponse, ChallengeResponse,
ChallengeTypes,
FlowErrorChallenge, FlowErrorChallenge,
HttpChallengeResponse, HttpChallengeResponse,
RedirectChallenge, RedirectChallenge,
@ -551,6 +552,7 @@ def to_stage_response(request: HttpRequest, source: HttpResponse) -> HttpRespons
return HttpChallengeResponse( return HttpChallengeResponse(
RedirectChallenge( RedirectChallenge(
{ {
"type": ChallengeTypes.REDIRECT,
"to": str(redirect_url), "to": str(redirect_url),
} }
) )
@ -559,6 +561,7 @@ def to_stage_response(request: HttpRequest, source: HttpResponse) -> HttpRespons
return HttpChallengeResponse( return HttpChallengeResponse(
ShellChallenge( ShellChallenge(
{ {
"type": ChallengeTypes.SHELL,
"body": source.render().content.decode("utf-8"), "body": source.render().content.decode("utf-8"),
} }
) )
@ -568,6 +571,7 @@ def to_stage_response(request: HttpRequest, source: HttpResponse) -> HttpRespons
return HttpChallengeResponse( return HttpChallengeResponse(
ShellChallenge( ShellChallenge(
{ {
"type": ChallengeTypes.SHELL,
"body": source.content.decode("utf-8"), "body": source.content.decode("utf-8"),
} }
) )

View File

@ -1,41 +0,0 @@
"""Interface views"""
from typing import Any
from django.shortcuts import get_object_or_404
from ua_parser.user_agent_parser import Parse
from authentik.core.views.interface import InterfaceView
from authentik.flows.models import Flow
class FlowInterfaceView(InterfaceView):
"""Flow interface"""
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
kwargs["flow"] = get_object_or_404(Flow, slug=self.kwargs.get("flow_slug"))
kwargs["inspector"] = "inspector" in self.request.GET
return super().get_context_data(**kwargs)
def compat_needs_sfe(self) -> bool:
"""Check if we need to use the simplified flow executor for compatibility"""
ua = Parse(self.request.META.get("HTTP_USER_AGENT", ""))
if ua["user_agent"]["family"] == "IE":
return True
# Only use SFE for Edge 18 and older, after Edge 18 MS switched to chromium which supports
# the default flow executor
if (
ua["user_agent"]["family"] == "Edge"
and int(ua["user_agent"]["major"]) <= 18 # noqa: PLR2004
): # noqa: PLR2004
return True
# https://github.com/AzureAD/microsoft-authentication-library-for-objc
# Used by Microsoft Teams/Office on macOS, and also uses a very outdated browser engine
if "PKeyAuth" in ua["string"]:
return True
return False
def get_template_names(self) -> list[str]:
if self.compat_needs_sfe() or "sfe" in self.request.GET:
return ["if/flow-sfe.html"]
return ["if/flow.html"]

View File

@ -50,6 +50,7 @@ cache:
timeout: 300 timeout: 300
timeout_flows: 300 timeout_flows: 300
timeout_policies: 300 timeout_policies: 300
timeout_reputation: 300
# channel: # channel:
# url: "" # url: ""
@ -115,9 +116,6 @@ events:
context_processors: context_processors:
geoip: "/geoip/GeoLite2-City.mmdb" geoip: "/geoip/GeoLite2-City.mmdb"
asn: "/geoip/GeoLite2-ASN.mmdb" asn: "/geoip/GeoLite2-ASN.mmdb"
compliance:
fips:
enabled: false
cert_discovery_dir: /certs cert_discovery_dir: /certs

View File

@ -19,7 +19,6 @@ from structlog.stdlib import get_logger
from authentik.core.models import User from authentik.core.models import User
from authentik.events.models import Event from authentik.events.models import Event
from authentik.lib.expression.exceptions import ControlFlowException
from authentik.lib.utils.http import get_http_session from authentik.lib.utils.http import get_http_session
from authentik.policies.models import Policy, PolicyBinding from authentik.policies.models import Policy, PolicyBinding
from authentik.policies.process import PolicyProcess from authentik.policies.process import PolicyProcess
@ -217,8 +216,7 @@ class BaseEvaluator:
# so the user only sees information relevant to them # so the user only sees information relevant to them
# and none of our surrounding error handling # and none of our surrounding error handling
exc.__traceback__ = exc.__traceback__.tb_next exc.__traceback__ = exc.__traceback__.tb_next
if not isinstance(exc, ControlFlowException): self.handle_error(exc, expression_source)
self.handle_error(exc, expression_source)
raise exc raise exc
return result return result

View File

@ -1,6 +0,0 @@
from authentik.lib.sentry import SentryIgnoredException
class ControlFlowException(SentryIgnoredException):
"""Exceptions used to control the flow from exceptions, not reported as a warning/
error in logs"""

View File

@ -102,8 +102,6 @@ def get_logger_config():
"gunicorn": "INFO", "gunicorn": "INFO",
"requests_mock": "WARNING", "requests_mock": "WARNING",
"hpack": "WARNING", "hpack": "WARNING",
"httpx": "WARNING",
"azure": "WARNING",
} }
for handler_name, level in handler_level_map.items(): for handler_name, level in handler_level_map.items():
base_config["loggers"][handler_name] = { base_config["loggers"][handler_name] = {

View File

@ -62,7 +62,7 @@ class DomainlessURLValidator(URLValidator):
r"^(?:[a-z0-9.+-]*)://" # scheme is validated separately r"^(?:[a-z0-9.+-]*)://" # scheme is validated separately
r"(?:[^\s:@/]+(?::[^\s:@/]*)?@)?" # user:pass authentication r"(?:[^\s:@/]+(?::[^\s:@/]*)?@)?" # user:pass authentication
r"(?:" + self.ipv4_re + "|" + self.ipv6_re + "|" + self.host_re + ")" r"(?:" + self.ipv4_re + "|" + self.ipv6_re + "|" + self.host_re + ")"
r"(?::\d{1,5})?" # port r"(?::\d{2,5})?" # port
r"(?:[/?#][^\s]*)?" # resource path r"(?:[/?#][^\s]*)?" # resource path
r"\Z", r"\Z",
re.IGNORECASE, re.IGNORECASE,
@ -88,7 +88,7 @@ class DomainlessFormattedURLValidator(DomainlessURLValidator):
r"^(?:[a-z0-9.+-]*)://" # scheme is validated separately r"^(?:[a-z0-9.+-]*)://" # scheme is validated separately
r"(?:[^\s:@/]+(?::[^\s:@/]*)?@)?" # user:pass authentication r"(?:[^\s:@/]+(?::[^\s:@/]*)?@)?" # user:pass authentication
r"(?:" + self.ipv4_re + "|" + self.ipv6_re + "|" + self.host_re + ")" r"(?:" + self.ipv4_re + "|" + self.ipv6_re + "|" + self.host_re + ")"
r"(?::\d{1,5})?" # port r"(?::\d{2,5})?" # port
r"(?:[/?#][^\s]*)?" # resource path r"(?:[/?#][^\s]*)?" # resource path
r"\Z", r"\Z",
re.IGNORECASE, re.IGNORECASE,

View File

@ -59,9 +59,8 @@ def sentry_init(**sentry_init_kwargs):
"_experiments": { "_experiments": {
"profiles_sample_rate": float(CONFIG.get("error_reporting.sample_rate", 0.1)), "profiles_sample_rate": float(CONFIG.get("error_reporting.sample_rate", 0.1)),
}, },
**sentry_init_kwargs,
**CONFIG.get_dict_from_b64_json("error_reporting.extra_args", {}),
} }
kwargs.update(**sentry_init_kwargs)
sentry_sdk_init( sentry_sdk_init(
dsn=CONFIG.get("error_reporting.sentry_dsn"), dsn=CONFIG.get("error_reporting.sentry_dsn"),

View File

@ -4,11 +4,8 @@ from django.db.models import QuerySet
from django.http import HttpRequest from django.http import HttpRequest
from authentik.core.expression.evaluator import PropertyMappingEvaluator from authentik.core.expression.evaluator import PropertyMappingEvaluator
from authentik.core.expression.exceptions import ( from authentik.core.expression.exceptions import PropertyMappingExpressionException
PropertyMappingExpressionException,
)
from authentik.core.models import PropertyMapping, User from authentik.core.models import PropertyMapping, User
from authentik.lib.expression.exceptions import ControlFlowException
class PropertyMappingManager: class PropertyMappingManager:
@ -60,8 +57,6 @@ class PropertyMappingManager:
mapping.set_context(user, request, **kwargs) mapping.set_context(user, request, **kwargs)
try: try:
value = mapping.evaluate(mapping.model.expression) value = mapping.evaluate(mapping.model.expression)
except (PropertyMappingExpressionException, ControlFlowException) as exc:
raise exc from exc
except Exception as exc: except Exception as exc:
raise PropertyMappingExpressionException(exc, mapping.model) from exc raise PropertyMappingExpressionException(exc, mapping.model) from exc
if value is None: if value is None:

View File

@ -8,7 +8,7 @@ from rest_framework.fields import BooleanField
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from authentik.core.api.utils import ModelSerializer, PassiveSerializer from authentik.core.api.utils import PassiveSerializer
from authentik.events.api.tasks import SystemTaskSerializer from authentik.events.api.tasks import SystemTaskSerializer
from authentik.lib.sync.outgoing.models import OutgoingSyncProvider from authentik.lib.sync.outgoing.models import OutgoingSyncProvider
@ -54,17 +54,3 @@ class OutgoingSyncProviderStatusMixin:
"is_running": not lock_acquired, "is_running": not lock_acquired,
} }
return Response(SyncStatusSerializer(status).data) return Response(SyncStatusSerializer(status).data)
class OutgoingSyncConnectionCreateMixin:
"""Mixin for connection objects that fetches remote data upon creation"""
def perform_create(self, serializer: ModelSerializer):
super().perform_create(serializer)
try:
instance = serializer.instance
client = instance.provider.client_for_model(instance.__class__)
client.update_single_attribute(instance)
instance.save()
except NotImplementedError:
pass

View File

@ -9,9 +9,9 @@ from structlog.stdlib import get_logger
from authentik.core.expression.exceptions import ( from authentik.core.expression.exceptions import (
PropertyMappingExpressionException, PropertyMappingExpressionException,
SkipObjectException,
) )
from authentik.events.models import Event, EventAction from authentik.events.models import Event, EventAction
from authentik.lib.expression.exceptions import ControlFlowException
from authentik.lib.sync.mapper import PropertyMappingManager from authentik.lib.sync.mapper import PropertyMappingManager
from authentik.lib.sync.outgoing.exceptions import NotFoundSyncException, StopSync from authentik.lib.sync.outgoing.exceptions import NotFoundSyncException, StopSync
from authentik.lib.utils.errors import exception_to_string from authentik.lib.utils.errors import exception_to_string
@ -91,9 +91,10 @@ class BaseOutgoingSyncClient[
} }
eval_kwargs.setdefault("user", None) eval_kwargs.setdefault("user", None)
for value in self.mapper.iter_eval(**eval_kwargs): for value in self.mapper.iter_eval(**eval_kwargs):
always_merger.merge(raw_final_object, value) try:
except ControlFlowException as exc: always_merger.merge(raw_final_object, value)
raise exc from exc except SkipObjectException as exc:
raise exc from exc
except PropertyMappingExpressionException as exc: except PropertyMappingExpressionException as exc:
# Value error can be raised when assigning invalid data to an attribute # Value error can be raised when assigning invalid data to an attribute
Event.new( Event.new(
@ -103,7 +104,7 @@ class BaseOutgoingSyncClient[
).save() ).save()
raise StopSync(exc, obj, exc.mapping) from exc raise StopSync(exc, obj, exc.mapping) from exc
if not raw_final_object: if not raw_final_object:
raise StopSync(ValueError("No mappings configured"), obj) raise StopSync(ValueError("No user mappings configured"), obj)
for key, value in defaults.items(): for key, value in defaults.items():
raw_final_object.setdefault(key, value) raw_final_object.setdefault(key, value)
return raw_final_object return raw_final_object
@ -114,8 +115,3 @@ class BaseOutgoingSyncClient[
pre-link any users/groups in the remote system with the respective pre-link any users/groups in the remote system with the respective
object in authentik based on a common identifier""" object in authentik based on a common identifier"""
raise NotImplementedError() raise NotImplementedError()
def update_single_attribute(self, connection: TConnection):
"""Update connection attributes on a connection object, when the connection
is manually created"""
raise NotImplementedError

View File

@ -2,7 +2,6 @@ from collections.abc import Callable
from django.core.paginator import Paginator from django.core.paginator import Paginator
from django.db.models import Model from django.db.models import Model
from django.db.models.query import Q
from django.db.models.signals import m2m_changed, post_save, pre_delete from django.db.models.signals import m2m_changed, post_save, pre_delete
from authentik.core.models import Group, User from authentik.core.models import Group, User
@ -35,9 +34,7 @@ def register_signals(
def model_post_save(sender: type[Model], instance: User | Group, created: bool, **_): def model_post_save(sender: type[Model], instance: User | Group, created: bool, **_):
"""Post save handler""" """Post save handler"""
if not provider_type.objects.filter( if not provider_type.objects.filter(backchannel_application__isnull=False).exists():
Q(backchannel_application__isnull=False) | Q(application__isnull=False)
).exists():
return return
task_sync_direct.delay(class_to_path(instance.__class__), instance.pk, Direction.add.value) task_sync_direct.delay(class_to_path(instance.__class__), instance.pk, Direction.add.value)
@ -46,9 +43,7 @@ def register_signals(
def model_pre_delete(sender: type[Model], instance: User | Group, **_): def model_pre_delete(sender: type[Model], instance: User | Group, **_):
"""Pre-delete handler""" """Pre-delete handler"""
if not provider_type.objects.filter( if not provider_type.objects.filter(backchannel_application__isnull=False).exists():
Q(backchannel_application__isnull=False) | Q(application__isnull=False)
).exists():
return return
task_sync_direct.delay( task_sync_direct.delay(
class_to_path(instance.__class__), instance.pk, Direction.remove.value class_to_path(instance.__class__), instance.pk, Direction.remove.value
@ -63,9 +58,7 @@ def register_signals(
"""Sync group membership""" """Sync group membership"""
if action not in ["post_add", "post_remove"]: if action not in ["post_add", "post_remove"]:
return return
if not provider_type.objects.filter( if not provider_type.objects.filter(backchannel_application__isnull=False).exists():
Q(backchannel_application__isnull=False) | Q(application__isnull=False)
).exists():
return return
# reverse: instance is a Group, pk_set is a list of user pks # reverse: instance is a Group, pk_set is a list of user pks
# non-reverse: instance is a User, pk_set is a list of groups # non-reverse: instance is a User, pk_set is a list of groups

View File

@ -5,7 +5,6 @@ from celery.exceptions import Retry
from celery.result import allow_join_result from celery.result import allow_join_result
from django.core.paginator import Paginator from django.core.paginator import Paginator
from django.db.models import Model, QuerySet from django.db.models import Model, QuerySet
from django.db.models.query import Q
from django.utils.text import slugify from django.utils.text import slugify
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from structlog.stdlib import BoundLogger, get_logger from structlog.stdlib import BoundLogger, get_logger
@ -15,7 +14,6 @@ from authentik.core.models import Group, User
from authentik.events.logs import LogEvent from authentik.events.logs import LogEvent
from authentik.events.models import TaskStatus from authentik.events.models import TaskStatus
from authentik.events.system_tasks import SystemTask from authentik.events.system_tasks import SystemTask
from authentik.events.utils import sanitize_item
from authentik.lib.sync.outgoing import PAGE_SIZE, PAGE_TIMEOUT from authentik.lib.sync.outgoing import PAGE_SIZE, PAGE_TIMEOUT
from authentik.lib.sync.outgoing.base import Direction from authentik.lib.sync.outgoing.base import Direction
from authentik.lib.sync.outgoing.exceptions import ( from authentik.lib.sync.outgoing.exceptions import (
@ -38,9 +36,7 @@ class SyncTasks:
self._provider_model = provider_model self._provider_model = provider_model
def sync_all(self, single_sync: Callable[[int], None]): def sync_all(self, single_sync: Callable[[int], None]):
for provider in self._provider_model.objects.filter( for provider in self._provider_model.objects.filter(backchannel_application__isnull=False):
Q(backchannel_application__isnull=False) | Q(application__isnull=False)
):
self.trigger_single_task(provider, single_sync) self.trigger_single_task(provider, single_sync)
def trigger_single_task(self, provider: OutgoingSyncProvider, sync_task: Callable[[int], None]): def trigger_single_task(self, provider: OutgoingSyncProvider, sync_task: Callable[[int], None]):
@ -65,8 +61,7 @@ class SyncTasks:
provider_pk=provider_pk, provider_pk=provider_pk,
) )
provider = self._provider_model.objects.filter( provider = self._provider_model.objects.filter(
Q(backchannel_application__isnull=False) | Q(application__isnull=False), pk=provider_pk, backchannel_application__isnull=False
pk=provider_pk,
).first() ).first()
if not provider: if not provider:
return return
@ -130,7 +125,6 @@ class SyncTasks:
try: try:
client.write(obj) client.write(obj)
except SkipObjectException: except SkipObjectException:
self.logger.debug("skipping object due to SkipObject", obj=obj)
continue continue
except BadRequestSyncException as exc: except BadRequestSyncException as exc:
self.logger.warning("failed to sync object", exc=exc, obj=obj) self.logger.warning("failed to sync object", exc=exc, obj=obj)
@ -150,8 +144,8 @@ class SyncTasks:
) )
), ),
log_level="warning", log_level="warning",
logger=f"{provider._meta.verbose_name}@{object_type}", logger="",
attributes={"arguments": exc.args[1:], "obj": sanitize_item(obj)}, attributes={"arguments": exc.args[1:]},
) )
) )
) )
@ -173,8 +167,7 @@ class SyncTasks:
) )
), ),
log_level="warning", log_level="warning",
logger=f"{provider._meta.verbose_name}@{object_type}", logger="",
attributes={"obj": sanitize_item(obj)},
) )
) )
) )
@ -191,8 +184,7 @@ class SyncTasks:
) )
), ),
log_level="warning", log_level="warning",
logger=f"{provider._meta.verbose_name}@{object_type}", logger="",
attributes={"obj": sanitize_item(obj)},
) )
) )
) )
@ -208,9 +200,7 @@ class SyncTasks:
if not instance: if not instance:
return return
operation = Direction(raw_op) operation = Direction(raw_op)
for provider in self._provider_model.objects.filter( for provider in self._provider_model.objects.filter(backchannel_application__isnull=False):
Q(backchannel_application__isnull=False) | Q(application__isnull=False)
):
client = provider.client_for_model(instance.__class__) client = provider.client_for_model(instance.__class__)
# Check if the object is allowed within the provider's restrictions # Check if the object is allowed within the provider's restrictions
queryset = provider.get_object_qs(instance.__class__) queryset = provider.get_object_qs(instance.__class__)
@ -239,9 +229,7 @@ class SyncTasks:
group = Group.objects.filter(pk=group_pk).first() group = Group.objects.filter(pk=group_pk).first()
if not group: if not group:
return return
for provider in self._provider_model.objects.filter( for provider in self._provider_model.objects.filter(backchannel_application__isnull=False):
Q(backchannel_application__isnull=False) | Q(application__isnull=False)
):
# Check if the object is allowed within the provider's restrictions # Check if the object is allowed within the provider's restrictions
queryset: QuerySet = provider.get_object_qs(Group) queryset: QuerySet = provider.get_object_qs(Group)
# The queryset we get from the provider must include the instance we've got given # The queryset we get from the provider must include the instance we've got given

View File

@ -6,21 +6,19 @@ from django_filters.filters import ModelMultipleChoiceFilter
from django_filters.filterset import FilterSet from django_filters.filterset import FilterSet
from drf_spectacular.utils import extend_schema from drf_spectacular.utils import extend_schema
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.exceptions import ValidationError from rest_framework.fields import BooleanField, CharField, DateTimeField
from rest_framework.fields import BooleanField, CharField, DateTimeField, SerializerMethodField
from rest_framework.relations import PrimaryKeyRelatedField from rest_framework.relations import PrimaryKeyRelatedField
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer, ValidationError
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from authentik import get_build_hash from authentik import get_build_hash
from authentik.core.api.providers import ProviderSerializer from authentik.core.api.providers import ProviderSerializer
from authentik.core.api.used_by import UsedByMixin from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import JSONDictField, ModelSerializer, PassiveSerializer from authentik.core.api.utils import JSONDictField, PassiveSerializer
from authentik.core.models import Provider from authentik.core.models import Provider
from authentik.enterprise.license import LicenseKey
from authentik.enterprise.providers.rac.models import RACProvider from authentik.enterprise.providers.rac.models import RACProvider
from authentik.lib.utils.time import timedelta_from_string, timedelta_string_validator
from authentik.outposts.api.service_connections import ServiceConnectionSerializer from authentik.outposts.api.service_connections import ServiceConnectionSerializer
from authentik.outposts.apps import MANAGED_OUTPOST, MANAGED_OUTPOST_NAME from authentik.outposts.apps import MANAGED_OUTPOST, MANAGED_OUTPOST_NAME
from authentik.outposts.models import ( from authentik.outposts.models import (
@ -50,10 +48,6 @@ class OutpostSerializer(ModelSerializer):
service_connection_obj = ServiceConnectionSerializer( service_connection_obj = ServiceConnectionSerializer(
source="service_connection", read_only=True source="service_connection", read_only=True
) )
refresh_interval_s = SerializerMethodField()
def get_refresh_interval_s(self, obj: Outpost) -> int:
return int(timedelta_from_string(obj.config.refresh_interval).total_seconds())
def validate_name(self, name: str) -> str: def validate_name(self, name: str) -> str:
"""Validate name (especially for embedded outpost)""" """Validate name (especially for embedded outpost)"""
@ -89,8 +83,7 @@ class OutpostSerializer(ModelSerializer):
def validate_config(self, config) -> dict: def validate_config(self, config) -> dict:
"""Check that the config has all required fields""" """Check that the config has all required fields"""
try: try:
parsed = from_dict(OutpostConfig, config) from_dict(OutpostConfig, config)
timedelta_string_validator(parsed.refresh_interval)
except DaciteError as exc: except DaciteError as exc:
raise ValidationError(f"Failed to validate config: {str(exc)}") from exc raise ValidationError(f"Failed to validate config: {str(exc)}") from exc
return config return config
@ -105,7 +98,6 @@ class OutpostSerializer(ModelSerializer):
"providers_obj", "providers_obj",
"service_connection", "service_connection",
"service_connection_obj", "service_connection_obj",
"refresh_interval_s",
"token_identifier", "token_identifier",
"config", "config",
"managed", "managed",
@ -128,7 +120,7 @@ class OutpostHealthSerializer(PassiveSerializer):
golang_version = CharField(read_only=True) golang_version = CharField(read_only=True)
openssl_enabled = BooleanField(read_only=True) openssl_enabled = BooleanField(read_only=True)
openssl_version = CharField(read_only=True) openssl_version = CharField(read_only=True)
fips_enabled = SerializerMethodField() fips_enabled = BooleanField(read_only=True)
version_should = CharField(read_only=True) version_should = CharField(read_only=True)
version_outdated = BooleanField(read_only=True) version_outdated = BooleanField(read_only=True)
@ -138,12 +130,6 @@ class OutpostHealthSerializer(PassiveSerializer):
hostname = CharField(read_only=True, required=False) hostname = CharField(read_only=True, required=False)
def get_fips_enabled(self, obj: dict) -> bool | None:
"""Get FIPS enabled"""
if not LicenseKey.get_total().is_valid():
return None
return obj["fips_enabled"]
class OutpostFilter(FilterSet): class OutpostFilter(FilterSet):
"""Filter for Outposts""" """Filter for Outposts"""

View File

@ -12,13 +12,13 @@ from rest_framework.decorators import action
from rest_framework.fields import BooleanField, CharField, ReadOnlyField from rest_framework.fields import BooleanField, CharField, ReadOnlyField
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import GenericViewSet, ModelViewSet from rest_framework.viewsets import GenericViewSet, ModelViewSet
from authentik.core.api.object_types import TypesMixin from authentik.core.api.object_types import TypesMixin
from authentik.core.api.used_by import UsedByMixin from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ( from authentik.core.api.utils import (
MetaNameSerializer, MetaNameSerializer,
ModelSerializer,
PassiveSerializer, PassiveSerializer,
) )
from authentik.outposts.models import ( from authentik.outposts.models import (

View File

@ -13,17 +13,16 @@ import authentik.outposts.models
def fix_missing_token_identifier(apps: Apps, schema_editor: BaseDatabaseSchemaEditor): def fix_missing_token_identifier(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
db_alias = schema_editor.connection.alias
User = apps.get_model("authentik_core", "User") User = apps.get_model("authentik_core", "User")
Token = apps.get_model("authentik_core", "Token") Token = apps.get_model("authentik_core", "Token")
from authentik.outposts.models import Outpost from authentik.outposts.models import Outpost
for outpost in Outpost.objects.using(db_alias).all().only("pk"): for outpost in Outpost.objects.using(schema_editor.connection.alias).all().only("pk"):
user_identifier = outpost.user_identifier user_identifier = outpost.user_identifier
users = User.objects.using(db_alias).filter(username=user_identifier) users = User.objects.filter(username=user_identifier)
if not users.exists(): if not users.exists():
continue continue
tokens = Token.objects.using(db_alias).filter(user=users.first()) tokens = Token.objects.filter(user=users.first())
for token in tokens: for token in tokens:
if token.identifier != outpost.token_identifier: if token.identifier != outpost.token_identifier:
token.identifier = outpost.token_identifier token.identifier = outpost.token_identifier
@ -38,8 +37,8 @@ def migrate_to_service_connection(apps: Apps, schema_editor: BaseDatabaseSchemaE
"authentik_outposts", "KubernetesServiceConnection" "authentik_outposts", "KubernetesServiceConnection"
) )
docker = DockerServiceConnection.objects.using(db_alias).filter(local=True).first() docker = DockerServiceConnection.objects.filter(local=True).first()
k8s = KubernetesServiceConnection.objects.using(db_alias).filter(local=True).first() k8s = KubernetesServiceConnection.objects.filter(local=True).first()
try: try:
for outpost in Outpost.objects.using(db_alias).all().exclude(deployment_type="custom"): for outpost in Outpost.objects.using(db_alias).all().exclude(deployment_type="custom"):
@ -55,21 +54,21 @@ def migrate_to_service_connection(apps: Apps, schema_editor: BaseDatabaseSchemaE
def remove_pb_prefix_users(apps: Apps, schema_editor: BaseDatabaseSchemaEditor): def remove_pb_prefix_users(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
db_alias = schema_editor.connection.alias alias = schema_editor.connection.alias
User = apps.get_model("authentik_core", "User") User = apps.get_model("authentik_core", "User")
Outpost = apps.get_model("authentik_outposts", "Outpost") Outpost = apps.get_model("authentik_outposts", "Outpost")
for outpost in Outpost.objects.using(db_alias).all(): for outpost in Outpost.objects.using(alias).all():
matching = User.objects.using(db_alias).filter(username=f"pb-outpost-{outpost.uuid.hex}") matching = User.objects.using(alias).filter(username=f"pb-outpost-{outpost.uuid.hex}")
if matching.exists(): if matching.exists():
matching.delete() matching.delete()
def update_config_prefix(apps: Apps, schema_editor: BaseDatabaseSchemaEditor): def update_config_prefix(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
db_alias = schema_editor.connection.alias alias = schema_editor.connection.alias
Outpost = apps.get_model("authentik_outposts", "Outpost") Outpost = apps.get_model("authentik_outposts", "Outpost")
for outpost in Outpost.objects.using(db_alias).all(): for outpost in Outpost.objects.using(alias).all():
config = outpost._config config = outpost._config
for key in list(config): for key in list(config):
if "passbook" in key: if "passbook" in key:

View File

@ -61,7 +61,6 @@ class OutpostConfig:
log_level: str = CONFIG.get("log_level") log_level: str = CONFIG.get("log_level")
object_naming_template: str = field(default="ak-outpost-%(name)s") object_naming_template: str = field(default="ak-outpost-%(name)s")
refresh_interval: str = "minutes=5"
container_image: str | None = field(default=None) container_image: str | None = field(default=None)

View File

@ -5,15 +5,13 @@ from collections import OrderedDict
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django_filters.filters import BooleanFilter, ModelMultipleChoiceFilter from django_filters.filters import BooleanFilter, ModelMultipleChoiceFilter
from django_filters.filterset import FilterSet from django_filters.filterset import FilterSet
from rest_framework.exceptions import ValidationError from rest_framework.serializers import ModelSerializer, PrimaryKeyRelatedField, ValidationError
from rest_framework.serializers import PrimaryKeyRelatedField
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from authentik.core.api.groups import GroupSerializer from authentik.core.api.groups import GroupSerializer
from authentik.core.api.used_by import UsedByMixin from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.users import UserSerializer from authentik.core.api.users import UserSerializer
from authentik.core.api.utils import ModelSerializer
from authentik.policies.api.policies import PolicySerializer from authentik.policies.api.policies import PolicySerializer
from authentik.policies.models import PolicyBinding, PolicyBindingModel from authentik.policies.models import PolicyBinding, PolicyBindingModel

View File

@ -6,9 +6,9 @@ from drf_spectacular.utils import OpenApiResponse, extend_schema
from guardian.shortcuts import get_objects_for_user from guardian.shortcuts import get_objects_for_user
from rest_framework import mixins from rest_framework import mixins
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.fields import SerializerMethodField
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer, SerializerMethodField
from rest_framework.viewsets import GenericViewSet from rest_framework.viewsets import GenericViewSet
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
@ -18,7 +18,6 @@ from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ( from authentik.core.api.utils import (
CacheSerializer, CacheSerializer,
MetaNameSerializer, MetaNameSerializer,
ModelSerializer,
) )
from authentik.events.logs import LogEventSerializer, capture_logs from authentik.events.logs import LogEventSerializer, capture_logs
from authentik.policies.api.exec import PolicyTestResultSerializer, PolicyTestSerializer from authentik.policies.api.exec import PolicyTestResultSerializer, PolicyTestSerializer

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