Compare commits

...

40 Commits

Author SHA1 Message Date
8f207c7504 release: 2024.6.3 2024-08-05 18:35:33 +02:00
34d30bb549 root: fix opencontainers ref (#10776)
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
# Conflicts:
#	poetry.lock
2024-08-05 16:30:54 +02:00
b4f04881e0 root: remove warnings (#10774)
* remove facebook sdk

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* switch to newer opencontainers fork

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
# Conflicts:
#	poetry.lock
2024-08-05 14:52:20 +02:00
5314485426 enterprise/rac: fix error when listing connection tokens as non-superuser (cherry-pick #10771) (#10773)
enterprise/rac: fix error when listing connection tokens as non-superuser (#10771)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-08-05 14:09:24 +02:00
ad6b6e4576 web: replace all occurences of the theme placeholder (cherry-pick #10749) (#10750)
web: replace all occurences of the theme placeholder (#10749)

Replace all occurences of the theme placeholder

This allows the placeholder to occur multiple times in the theme url.

Signed-off-by: Chasethechicken <neuringe1234@gmail.com>
Co-authored-by: Chasethechicken <neuringe1234@gmail.com>
2024-08-05 11:57:32 +02:00
fb9aa9d7f7 sources/scim: fix duplicate service account users and changing token (cherry-pick #10735) (#10737)
sources/scim: fix duplicate service account users and changing token (#10735)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-08-02 14:12:23 +02:00
fe7662f80d web: fix theme not applying to document correctly (cherry-pick #10721) (#10722)
web: fix theme not applying to document correctly (#10721)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-08-01 15:09:38 +02:00
d6904b6aa1 release: 2024.6.2 2024-07-31 16:54:24 +02:00
cd581efacd tests/e2e: fix ldap tests following #10270 (cherry-pick #10288) (#10703)
tests/e2e: fix ldap tests following #10270 (#10288)

Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-07-31 16:01:32 +02:00
6c159d120b outposts: ensure minimum refresh interval (cherry-pick #10701) (#10702)
outposts: ensure minimum refresh interval (#10701)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-07-31 14:59:14 +02:00
4ddd4e7f88 outposts: make refresh interval configurable (cherry-pick #10138) (#10700)
* outposts: make refresh interval configurable (#10138)

* outposts: make refresh interval configurable

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* frontend

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* black again

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* switch to using config attribute

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* lint

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

---------

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* bump api

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
2024-07-31 14:38:09 +02:00
441912414f web/admin: show matching user reputation scores in user details (cherry-pick #10276) (#10699)
* web/admin: show matching user reputation scores in user details (#10276)

Co-authored-by: Jens Langhammer <jens@goauthentik.io>

* bump api

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
2024-07-31 14:37:58 +02:00
9e177ed5c0 web: fix dark theme and theme switch (#10667)
* base locale off of ak-element

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* revert temp theme fixes

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix theme switching

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add basic support for theme-different images

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* sort outposts in card

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* set default theme based on pre-hydrated brand settings

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* activate global theme before root in shadow dom

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* logging

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* when using _applyTheme, check media matcher

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add docs

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
# Conflicts:
#	web/src/elements/Base.ts
#	website/docs/core/brands.md
2024-07-29 20:26:44 +02:00
881548176f events: associate login_failed events to a user if possible (cherry-pick #10270) (#10676)
* events: associate login_failed events to a user if possible (#10270)

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* format

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
2024-07-29 20:00:13 +02:00
56739d0dc4 web/flows: remove continue button from AutoSubmit stage (cherry-pick #10253) (#10677)
web/flows: remove continue button from AutoSubmit stage (#10253)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2024-07-29 19:32:29 +02:00
b23972e9c9 lifecycle: only create tenant media root if needed (cherry-pick #10616) (#10617)
lifecycle: only create tenant media root if needed (#10616)

Co-authored-by: Jens L. <jens@goauthentik.io>
2024-07-24 21:12:48 +02:00
0a9595089e web/admin: fix missing SAML Provider ECDSA options (cherry-pick #10612) (#10618)
web/admin: fix missing SAML Provider ECDSA options (#10612)

* web/admin: fix missing SAML Provider ECDSA options



* deduplicate



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-07-24 21:12:23 +02:00
72c22b5fab core: remove html language tag for pages that are translated (cherry-pick #10611) (#10613)
core: remove html language tag for pages that are translated (#10611)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-07-24 19:42:48 +02:00
84cdbb0a03 events: fix race condition (cherry-pick #10602) (#10609)
events: fix race condition (#10602)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-07-24 16:53:03 +02:00
9fc659f121 stages/prompt: fix prompt not editable with invalid expression (cherry-pick #10603) (#10604)
stages/prompt: fix prompt not editable with invalid expression (#10603)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-07-24 14:36:33 +02:00
db6abf61b8 lib/sync: handle SkipObject in direct triggered tasks (cherry-pick #10590) (#10591)
lib/sync: handle SkipObject in direct triggered tasks (#10590)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-07-23 15:38:37 +02:00
6426a1d177 core: improve error handling on ASGI level (cherry-pick #10547) (#10552)
core: improve error handling on ASGI level (#10547)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-07-19 17:19:29 +02:00
9075270b01 release: 2024.6.1 2024-07-11 21:45:54 +02:00
d17a39a431 website/docs: add 2024.6.1 release notes (cherry-pick #10456) (#10458)
website/docs: add 2024.6.1 release notes (#10456)

* website/docs: add 2024.6.1 release notes



* update



* fix version requirement for sfe



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2024-07-11 19:11:28 +02:00
db1d091d2e core: revert backchannel only filtering (cherry-pick #10455) (#10457)
core: revert backchannel only filtering (#10455)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2024-07-11 16:58:29 +02:00
f98204e78e core: fix source flow_manager not resuming flow when linking (cherry-pick #10436) (#10438)
core: fix source flow_manager not resuming flow when linking (#10436)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2024-07-10 15:20:15 +02:00
3f663cab0f web/admin: fix access token list calling wrong API (cherry-pick #10434) (#10435)
web/admin: fix access token list calling wrong API (#10434)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2024-07-10 14:17:47 +02:00
3fe129e107 core: fix migrations missing using db_alias (cherry-pick #10409) (#10410)
core: fix migrations missing using db_alias (#10409)

Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-07-09 10:48:29 +02:00
f26d41aef9 web: bump API Client version (#10389)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
# Conflicts:
#	web/package-lock.json
#	web/package.json
2024-07-05 20:49:31 +02:00
5d8b5998ae web/flows: Simplified flow executor (#10296)
* initial sfe

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* build sfe

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* downgrade bootstrap

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix path

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* make IE compatible

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix query string missing

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add autosubmit stage

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add background image

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add code support

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add support for combo ident/password

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix logo rendering

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* only use for edge 18 and before

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix lint

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add docs

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add webauthn support

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* migrate to TS for some creature comforts

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix ci

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* dedupe dependabot

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* use API client...kinda

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add more docs

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add more polyfills yay

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* turn powered by into span

prevent issues in restricted browsers where users might not be able to return

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* allow non-link footer entries

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix tsc errors

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* Apply suggestions from code review

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Jens L. <jens@beryju.org>

* auto switch for macos

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* reword

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* Update website/docs/flow/executors/if-flow.md

Signed-off-by: Jens L. <jens@beryju.org>

* format

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Signed-off-by: Jens L. <jens@beryju.org>
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
# Conflicts:
#	.github/workflows/ci-web.yml
#	Dockerfile
#	website/developer-docs/api/flow-executor.md
2024-07-05 20:43:14 +02:00
7a5e136346 stages/authenticator_validate: fix friendly_name being required (cherry-pick #10382) (#10385)
stages/authenticator_validate: fix friendly_name being required (#10382)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2024-07-05 15:50:14 +02:00
bfbab6357a sources/oauth: fix link not being saved (cherry-pick #10374) (#10376)
sources/oauth: fix link not being saved (#10374)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2024-07-04 16:58:38 +02:00
5997b93f15 sources/saml: fix pickle error, add saml auth tests (cherry-pick #10348) (#10352)
sources/saml: fix pickle error, add saml auth tests (#10348)

* test with persistent nameid



* fix pickle



* user_write: dont attempt to write to read only property



* add test for enroll + auth



* unwrap lazy user



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2024-07-03 18:34:22 +02:00
6cdae09dc0 providers/saml: fix metadata import error handling (cherry-pick #10349) (#10350)
Co-authored-by: Jens L <jens@goauthentik.io>
fix metadata import error handling (#10349)
2024-07-03 16:01:50 +00:00
ff0ef7a2b3 web: set noopener and noreferrer on all external links (#10304)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-07-02 14:54:03 +02:00
3986104a20 provider/scim: Fix exception handling for missing ServiceProviderConfig (cherry-pick #10322) (#10335)
provider/scim: Fix exception handling for missing ServiceProviderConfig (#10322)

Co-authored-by: Michael Poutre <m1kep.my.mail@gmail.com>
2024-07-02 13:53:27 +02:00
1aa60e7864 core: remove transitionary old JS urls (cherry-pick #10317) (#10321)
core: remove transitionary old JS urls (#10317)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2024-07-01 21:00:05 +02:00
045578dd07 web/flows: remove background image link (cherry-pick #10318) (#10320)
web/flows: remove background image link (#10318)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2024-07-01 20:28:30 +02:00
f23d70dc75 stages/user_login: fix ?next parameter not carried through broken session binding (cherry-pick #10301) (#10302)
stages/user_login: fix ?next parameter not carried through broken session binding (#10301)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2024-06-29 23:17:13 +02:00
496f3426d9 website/docs: update geoip and asn documentation following field changes (cherry-pick #10265) (#10266)
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
2024-06-27 13:26:31 +00:00
117 changed files with 7969 additions and 4676 deletions

View File

@ -1,5 +1,5 @@
[bumpversion] [bumpversion]
current_version = 2024.6.0 current_version = 2024.6.3
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*))?

View File

@ -21,7 +21,10 @@ updates:
labels: labels:
- dependencies - dependencies
- package-ecosystem: npm - package-ecosystem: npm
directory: "/web" directories:
- "/web"
- "/tests/wdio"
- "/web/sfe"
schedule: schedule:
interval: daily interval: daily
time: "04:00" time: "04:00"
@ -30,7 +33,6 @@ 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:
@ -56,38 +58,6 @@ 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,7 +31,12 @@ 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

@ -20,6 +20,16 @@ jobs:
project: project:
- web - web
- tests/wdio - tests/wdio
include:
- command: tsc
project: web
extra_setup: |
cd sfe/ && npm ci
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

View File

@ -30,7 +30,12 @@ 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 ./package.json /work
@ -38,7 +43,9 @@ 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

@ -2,7 +2,7 @@
from os import environ from os import environ
__version__ = "2024.6.0" __version__ = "2024.6.3"
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH" ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"

View File

@ -24,7 +24,7 @@ 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) href = CharField(read_only=True, allow_null=True)
name = CharField(read_only=True) name = CharField(read_only=True)

View File

@ -7,12 +7,13 @@ 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.only("is_backchannel"): for obj in model.objects.using(db_alias).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

@ -212,7 +212,7 @@ class SourceFlowManager:
def _prepare_flow( def _prepare_flow(
self, self,
flow: Flow, flow: Flow | None,
connection: UserSourceConnection, connection: UserSourceConnection,
stages: list[StageView] | None = None, stages: list[StageView] | None = None,
**kwargs, **kwargs,
@ -309,7 +309,9 @@ 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)
# Connection has already been saved if SESSION_KEY_OVERRIDE_FLOW_TOKEN in self.request.session:
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", () => { window.addEventListener("DOMContentLoaded", function () {
{% for message in messages %} {% for message in messages %}
window.dispatchEvent( window.dispatchEvent(
new CustomEvent("ak-message", { new CustomEvent("ak-message", {

View File

@ -4,7 +4,7 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html>
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">

View File

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

View File

@ -17,11 +17,5 @@ def versioned_script(path: str) -> str:
f'<script src="{static_loader(path.replace("%v", get_full_version()))}' f'<script src="{static_loader(path.replace("%v", get_full_version()))}'
'" type="module"></script>' '" type="module"></script>'
), ),
# Legacy method of loading scripts used as a fallback, without the version in the filename
# TODO: Remove after 2024.6 or later
(
f'<script src="{static_loader(path.replace("-%v", ""))}?'
f'version={get_full_version()}" type="module"></script>'
),
] ]
return mark_safe("".join(returned_lines)) # nosec return mark_safe("".join(returned_lines)) # nosec

View File

@ -20,8 +20,9 @@ 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 FlowInterfaceView, InterfaceView from authentik.core.views.interface import 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
@ -53,6 +54,8 @@ 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

@ -3,7 +3,6 @@
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
@ -11,7 +10,6 @@ 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):
@ -25,14 +23,3 @@ 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

@ -34,6 +34,12 @@ class ConnectionTokenSerializer(EnterpriseRequiredMixin, ModelSerializer):
] ]
class ConnectionTokenOwnerFilter(OwnerFilter):
"""Owner filter for connection tokens (checks session's user)"""
owner_key = "session__user"
class ConnectionTokenViewSet( class ConnectionTokenViewSet(
mixins.RetrieveModelMixin, mixins.RetrieveModelMixin,
mixins.UpdateModelMixin, mixins.UpdateModelMixin,
@ -50,4 +56,9 @@ class ConnectionTokenViewSet(
search_fields = ["endpoint__name", "provider__name"] search_fields = ["endpoint__name", "provider__name"]
ordering = ["endpoint__name", "provider__name"] ordering = ["endpoint__name", "provider__name"]
permission_classes = [OwnerSuperuserPermissions] permission_classes = [OwnerSuperuserPermissions]
filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter] filter_backends = [
ConnectionTokenOwnerFilter,
DjangoFilterBackend,
OrderingFilter,
SearchFilter,
]

View File

@ -5,7 +5,6 @@ from channels.sessions import CookieMiddleware
from django.urls import path from django.urls import path
from django.views.decorators.csrf import ensure_csrf_cookie from django.views.decorators.csrf import ensure_csrf_cookie
from authentik.core.channels import TokenOutpostMiddleware
from authentik.enterprise.providers.rac.api.connection_tokens import ConnectionTokenViewSet from authentik.enterprise.providers.rac.api.connection_tokens import ConnectionTokenViewSet
from authentik.enterprise.providers.rac.api.endpoints import EndpointViewSet from authentik.enterprise.providers.rac.api.endpoints import EndpointViewSet
from authentik.enterprise.providers.rac.api.property_mappings import RACPropertyMappingViewSet from authentik.enterprise.providers.rac.api.property_mappings import RACPropertyMappingViewSet
@ -13,6 +12,7 @@ from authentik.enterprise.providers.rac.api.providers import RACProviderViewSet
from authentik.enterprise.providers.rac.consumer_client import RACClientConsumer from authentik.enterprise.providers.rac.consumer_client import RACClientConsumer
from authentik.enterprise.providers.rac.consumer_outpost import RACOutpostConsumer from authentik.enterprise.providers.rac.consumer_outpost import RACOutpostConsumer
from authentik.enterprise.providers.rac.views import RACInterface, RACStartView from authentik.enterprise.providers.rac.views import RACInterface, RACStartView
from authentik.outposts.channels import TokenOutpostMiddleware
from authentik.root.asgi_middleware import SessionMiddleware from authentik.root.asgi_middleware import SessionMiddleware
from authentik.root.middleware import ChannelsLoggingMiddleware from authentik.root.middleware import ChannelsLoggingMiddleware

View File

@ -35,6 +35,7 @@ IGNORED_MODELS = tuple(
_CTX_OVERWRITE_USER = ContextVar[User | None]("authentik_events_log_overwrite_user", default=None) _CTX_OVERWRITE_USER = ContextVar[User | None]("authentik_events_log_overwrite_user", default=None)
_CTX_IGNORE = ContextVar[bool]("authentik_events_log_ignore", default=False) _CTX_IGNORE = ContextVar[bool]("authentik_events_log_ignore", default=False)
_CTX_REQUEST = ContextVar[HttpRequest | None]("authentik_events_log_request", default=None)
def should_log_model(model: Model) -> bool: def should_log_model(model: Model) -> bool:
@ -149,11 +150,13 @@ class AuditMiddleware:
m2m_changed.disconnect(dispatch_uid=request.request_id) m2m_changed.disconnect(dispatch_uid=request.request_id)
def __call__(self, request: HttpRequest) -> HttpResponse: def __call__(self, request: HttpRequest) -> HttpResponse:
_CTX_REQUEST.set(request)
self.connect(request) self.connect(request)
response = self.get_response(request) response = self.get_response(request)
self.disconnect(request) self.disconnect(request)
_CTX_REQUEST.set(None)
return response return response
def process_exception(self, request: HttpRequest, exception: Exception): def process_exception(self, request: HttpRequest, exception: Exception):
@ -167,7 +170,7 @@ class AuditMiddleware:
thread = EventNewThread( thread = EventNewThread(
EventAction.SUSPICIOUS_REQUEST, EventAction.SUSPICIOUS_REQUEST,
request, request,
message=str(exception), message=exception_to_string(exception),
) )
thread.run() thread.run()
elif before_send({}, {"exc_info": (None, exception, None)}) is not None: elif before_send({}, {"exc_info": (None, exception, None)}) is not None:
@ -192,6 +195,8 @@ class AuditMiddleware:
return return
if _CTX_IGNORE.get(): if _CTX_IGNORE.get():
return return
if request.request_id != _CTX_REQUEST.get().request_id:
return
user = self.get_user(request) user = self.get_user(request)
action = EventAction.MODEL_CREATED if created else EventAction.MODEL_UPDATED action = EventAction.MODEL_CREATED if created else EventAction.MODEL_UPDATED
@ -205,6 +210,8 @@ class AuditMiddleware:
return return
if _CTX_IGNORE.get(): if _CTX_IGNORE.get():
return return
if request.request_id != _CTX_REQUEST.get().request_id:
return
user = self.get_user(request) user = self.get_user(request)
EventNewThread( EventNewThread(
@ -230,6 +237,8 @@ class AuditMiddleware:
return return
if _CTX_IGNORE.get(): if _CTX_IGNORE.get():
return return
if request.request_id != _CTX_REQUEST.get().request_id:
return
user = self.get_user(request) user = self.get_user(request)
EventNewThread( EventNewThread(

View File

@ -238,6 +238,8 @@ class Event(SerializerModel, ExpiringModel):
"args": cleanse_dict(QueryDict(request.META.get("QUERY_STRING", ""))), "args": cleanse_dict(QueryDict(request.META.get("QUERY_STRING", ""))),
"user_agent": request.META.get("HTTP_USER_AGENT", ""), "user_agent": request.META.get("HTTP_USER_AGENT", ""),
} }
if hasattr(request, "request_id"):
self.context["http_request"]["request_id"] = request.request_id
# Special case for events created during flow execution # Special case for events created during flow execution
# since they keep the http query within a wrapped query # since they keep the http query within a wrapped query
if QS_QUERY in self.context["http_request"]["args"]: if QS_QUERY in self.context["http_request"]["args"]:

View File

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

View File

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

View File

@ -0,0 +1,54 @@
{% 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

@ -0,0 +1,41 @@
"""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

@ -2,6 +2,7 @@ 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
@ -34,7 +35,9 @@ 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(backchannel_application__isnull=False).exists(): if not provider_type.objects.filter(
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)
@ -43,7 +46,9 @@ 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(backchannel_application__isnull=False).exists(): if not provider_type.objects.filter(
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
@ -58,7 +63,9 @@ 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(backchannel_application__isnull=False).exists(): if not provider_type.objects.filter(
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,6 +5,7 @@ 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
@ -37,7 +38,9 @@ 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(backchannel_application__isnull=False): for provider in self._provider_model.objects.filter(
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]):
@ -62,7 +65,8 @@ class SyncTasks:
provider_pk=provider_pk, provider_pk=provider_pk,
) )
provider = self._provider_model.objects.filter( provider = self._provider_model.objects.filter(
pk=provider_pk, backchannel_application__isnull=False Q(backchannel_application__isnull=False) | Q(application__isnull=False),
pk=provider_pk,
).first() ).first()
if not provider: if not provider:
return return
@ -204,7 +208,9 @@ 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(backchannel_application__isnull=False): for provider in self._provider_model.objects.filter(
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__)
@ -223,6 +229,8 @@ class SyncTasks:
client.delete(instance) client.delete(instance)
except TransientSyncException as exc: except TransientSyncException as exc:
raise Retry() from exc raise Retry() from exc
except SkipObjectException:
continue
except StopSync as exc: except StopSync as exc:
self.logger.warning(exc, provider_pk=provider.pk) self.logger.warning(exc, provider_pk=provider.pk)
@ -233,7 +241,9 @@ 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(backchannel_application__isnull=False): for provider in self._provider_model.objects.filter(
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
@ -251,5 +261,7 @@ class SyncTasks:
client.update_group(group, operation, pk_set) client.update_group(group, operation, pk_set)
except TransientSyncException as exc: except TransientSyncException as exc:
raise Retry() from exc raise Retry() from exc
except SkipObjectException:
continue
except StopSync as exc: except StopSync as exc:
self.logger.warning(exc, provider_pk=provider.pk) self.logger.warning(exc, provider_pk=provider.pk)

View File

@ -20,6 +20,7 @@ from authentik.core.api.utils import JSONDictField, ModelSerializer, PassiveSeri
from authentik.core.models import Provider from authentik.core.models import Provider
from authentik.enterprise.license import LicenseKey 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 (
@ -49,6 +50,10 @@ 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)"""
@ -84,7 +89,8 @@ 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:
from_dict(OutpostConfig, config) parsed = 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
@ -99,6 +105,7 @@ 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",

View File

@ -13,16 +13,17 @@ 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(schema_editor.connection.alias).all().only("pk"): for outpost in Outpost.objects.using(db_alias).all().only("pk"):
user_identifier = outpost.user_identifier user_identifier = outpost.user_identifier
users = User.objects.filter(username=user_identifier) users = User.objects.using(db_alias).filter(username=user_identifier)
if not users.exists(): if not users.exists():
continue continue
tokens = Token.objects.filter(user=users.first()) tokens = Token.objects.using(db_alias).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
@ -37,8 +38,8 @@ def migrate_to_service_connection(apps: Apps, schema_editor: BaseDatabaseSchemaE
"authentik_outposts", "KubernetesServiceConnection" "authentik_outposts", "KubernetesServiceConnection"
) )
docker = DockerServiceConnection.objects.filter(local=True).first() docker = DockerServiceConnection.objects.using(db_alias).filter(local=True).first()
k8s = KubernetesServiceConnection.objects.filter(local=True).first() k8s = KubernetesServiceConnection.objects.using(db_alias).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"):
@ -54,21 +55,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):
alias = schema_editor.connection.alias db_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(alias).all(): for outpost in Outpost.objects.using(db_alias).all():
matching = User.objects.using(alias).filter(username=f"pb-outpost-{outpost.uuid.hex}") matching = User.objects.using(db_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):
alias = schema_editor.connection.alias db_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(alias).all(): for outpost in Outpost.objects.using(db_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,6 +61,7 @@ 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

@ -2,7 +2,6 @@
from dataclasses import asdict from dataclasses import asdict
from channels.exceptions import DenyConnection
from channels.routing import URLRouter from channels.routing import URLRouter
from channels.testing import WebsocketCommunicator from channels.testing import WebsocketCommunicator
from django.test import TransactionTestCase from django.test import TransactionTestCase
@ -37,9 +36,8 @@ class TestOutpostWS(TransactionTestCase):
communicator = WebsocketCommunicator( communicator = WebsocketCommunicator(
URLRouter(websocket.websocket_urlpatterns), f"/ws/outpost/{self.outpost.pk}/" URLRouter(websocket.websocket_urlpatterns), f"/ws/outpost/{self.outpost.pk}/"
) )
with self.assertRaises(DenyConnection): connected, _ = await communicator.connect()
connected, _ = await communicator.connect() self.assertFalse(connected)
self.assertFalse(connected)
async def test_auth_valid(self): async def test_auth_valid(self):
"""Test auth with token""" """Test auth with token"""

View File

@ -2,13 +2,13 @@
from django.urls import path from django.urls import path
from authentik.core.channels import TokenOutpostMiddleware
from authentik.outposts.api.outposts import OutpostViewSet from authentik.outposts.api.outposts import OutpostViewSet
from authentik.outposts.api.service_connections import ( from authentik.outposts.api.service_connections import (
DockerServiceConnectionViewSet, DockerServiceConnectionViewSet,
KubernetesServiceConnectionViewSet, KubernetesServiceConnectionViewSet,
ServiceConnectionViewSet, ServiceConnectionViewSet,
) )
from authentik.outposts.channels import TokenOutpostMiddleware
from authentik.outposts.consumer import OutpostConsumer from authentik.outposts.consumer import OutpostConsumer
from authentik.root.middleware import ChannelsLoggingMiddleware from authentik.root.middleware import ChannelsLoggingMiddleware

View File

@ -1,6 +1,8 @@
"""Reputation policy API Views""" """Reputation policy API Views"""
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django_filters.filters import BaseInFilter, CharFilter
from django_filters.filterset import FilterSet
from rest_framework import mixins from rest_framework import mixins
from rest_framework.exceptions import ValidationError from rest_framework.exceptions import ValidationError
from rest_framework.viewsets import GenericViewSet, ModelViewSet from rest_framework.viewsets import GenericViewSet, ModelViewSet
@ -11,6 +13,10 @@ from authentik.policies.api.policies import PolicySerializer
from authentik.policies.reputation.models import Reputation, ReputationPolicy from authentik.policies.reputation.models import Reputation, ReputationPolicy
class CharInFilter(BaseInFilter, CharFilter):
pass
class ReputationPolicySerializer(PolicySerializer): class ReputationPolicySerializer(PolicySerializer):
"""Reputation Policy Serializer""" """Reputation Policy Serializer"""
@ -38,6 +44,16 @@ class ReputationPolicyViewSet(UsedByMixin, ModelViewSet):
ordering = ["name"] ordering = ["name"]
class ReputationFilter(FilterSet):
"""Filter for reputation"""
identifier_in = CharInFilter(field_name="identifier", lookup_expr="in")
class Meta:
model = Reputation
fields = ["identifier", "ip", "score"]
class ReputationSerializer(ModelSerializer): class ReputationSerializer(ModelSerializer):
"""Reputation Serializer""" """Reputation Serializer"""
@ -66,5 +82,5 @@ class ReputationViewSet(
queryset = Reputation.objects.all() queryset = Reputation.objects.all()
serializer_class = ReputationSerializer serializer_class = ReputationSerializer
search_fields = ["identifier", "ip", "score"] search_fields = ["identifier", "ip", "score"]
filterset_fields = ["identifier", "ip", "score"] filterset_class = ReputationFilter
ordering = ["ip"] ordering = ["ip"]

View File

@ -268,7 +268,7 @@ class SAMLProviderViewSet(UsedByMixin, ModelViewSet):
except ValueError as exc: # pragma: no cover except ValueError as exc: # pragma: no cover
LOGGER.warning(str(exc)) LOGGER.warning(str(exc))
raise ValidationError( raise ValidationError(
_("Failed to import Metadata: {messages}".format_map({"message": str(exc)})), _("Failed to import Metadata: {messages}".format_map({"messages": str(exc)})),
) from None ) from None
return Response(status=204) return Response(status=204)

View File

@ -89,6 +89,6 @@ class SCIMClient[TModel: "Model", TConnection: "Model", TSchema: "BaseModel"](
return ServiceProviderConfiguration.model_validate( return ServiceProviderConfiguration.model_validate(
self._request("GET", "/ServiceProviderConfig") self._request("GET", "/ServiceProviderConfig")
) )
except (ValidationError, SCIMRequestException) as exc: except (ValidationError, SCIMRequestException, NotFoundSyncException) as exc:
self.logger.warning("failed to get ServiceProviderConfig", exc=exc) self.logger.warning("failed to get ServiceProviderConfig", exc=exc)
return default_config return default_config

View File

@ -274,9 +274,13 @@ class ChannelsLoggingMiddleware:
self.log(scope) self.log(scope)
try: try:
return await self.inner(scope, receive, send) return await self.inner(scope, receive, send)
except DenyConnection:
return await send({"type": "websocket.close"})
except Exception as exc: except Exception as exc:
if settings.DEBUG:
raise exc
LOGGER.warning("Exception in ASGI application", exc=exc) LOGGER.warning("Exception in ASGI application", exc=exc)
raise DenyConnection() from None return await send({"type": "websocket.close"})
def log(self, scope: dict, **kwargs): def log(self, scope: dict, **kwargs):
"""Log request""" """Log request"""

View File

@ -31,9 +31,9 @@ def set_default_group_mappings(apps: Apps, schema_editor):
db_alias = schema_editor.connection.alias db_alias = schema_editor.connection.alias
for source in LDAPSource.objects.using(db_alias).all(): for source in LDAPSource.objects.using(db_alias).all():
if source.property_mappings_group.exists(): if source.property_mappings_group.using(db_alias).exists():
continue continue
source.property_mappings_group.set( source.property_mappings_group.using(db_alias).set(
LDAPPropertyMapping.objects.using(db_alias).filter( LDAPPropertyMapping.objects.using(db_alias).filter(
managed="goauthentik.io/sources/ldap/default-name" managed="goauthentik.io/sources/ldap/default-name"
) )

View File

@ -2,9 +2,6 @@
from typing import Any from typing import Any
from facebook import GraphAPI
from authentik.sources.oauth.clients.oauth2 import OAuth2Client
from authentik.sources.oauth.types.registry import SourceType, registry from authentik.sources.oauth.types.registry import SourceType, registry
from authentik.sources.oauth.views.callback import OAuthCallback from authentik.sources.oauth.views.callback import OAuthCallback
from authentik.sources.oauth.views.redirect import OAuthRedirect from authentik.sources.oauth.views.redirect import OAuthRedirect
@ -19,19 +16,9 @@ class FacebookOAuthRedirect(OAuthRedirect):
} }
class FacebookOAuth2Client(OAuth2Client):
"""Facebook OAuth2 Client"""
def get_profile_info(self, token: dict[str, str]) -> dict[str, Any] | None:
api = GraphAPI(access_token=token["access_token"])
return api.get_object("me", fields="id,name,email")
class FacebookOAuth2Callback(OAuthCallback): class FacebookOAuth2Callback(OAuthCallback):
"""Facebook OAuth2 Callback""" """Facebook OAuth2 Callback"""
client_class = FacebookOAuth2Client
def get_user_enroll_context( def get_user_enroll_context(
self, self,
info: dict[str, Any], info: dict[str, Any],

View File

@ -10,6 +10,8 @@ from authentik.sources.saml.processors import constants
def update_algorithms(apps: Apps, schema_editor: BaseDatabaseSchemaEditor): def update_algorithms(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
db_alias = schema_editor.connection.alias
SAMLSource = apps.get_model("authentik_sources_saml", "SAMLSource") SAMLSource = apps.get_model("authentik_sources_saml", "SAMLSource")
signature_translation_map = { signature_translation_map = {
"rsa-sha1": constants.RSA_SHA1, "rsa-sha1": constants.RSA_SHA1,
@ -22,7 +24,7 @@ def update_algorithms(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
"sha256": constants.SHA256, "sha256": constants.SHA256,
} }
for source in SAMLSource.objects.all(): for source in SAMLSource.objects.using(db_alias).all():
source.signature_algorithm = signature_translation_map.get( source.signature_algorithm = signature_translation_map.get(
source.signature_algorithm, constants.RSA_SHA256 source.signature_algorithm, constants.RSA_SHA256
) )

View File

@ -10,6 +10,7 @@ from django.core.cache import cache
from django.core.exceptions import SuspiciousOperation from django.core.exceptions import SuspiciousOperation
from django.http import HttpRequest from django.http import HttpRequest
from django.utils.timezone import now from django.utils.timezone import now
from lxml import etree # nosec
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from authentik.core.models import ( from authentik.core.models import (
@ -240,7 +241,7 @@ class ResponseProcessor:
name_id.text, name_id.text,
delete_none_values(self.get_attributes()), delete_none_values(self.get_attributes()),
) )
flow_manager.policy_context["saml_response"] = self._root flow_manager.policy_context["saml_response"] = etree.tostring(self._root)
return flow_manager return flow_manager

View File

@ -1,7 +1,5 @@
"""SCIM Source""" """SCIM Source"""
from uuid import uuid4
from django.db import models from django.db import models
from django.templatetags.static import static from django.templatetags.static import static
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@ -19,8 +17,6 @@ class SCIMSource(Source):
@property @property
def service_account_identifier(self) -> str: def service_account_identifier(self) -> str:
if not self.pk:
self.pk = uuid4()
return f"ak-source-scim-{self.pk}" return f"ak-source-scim-{self.pk}"
@property @property

View File

@ -1,41 +1,44 @@
from django.db.models import Model from django.db.models import Model
from django.db.models.signals import pre_delete, pre_save from django.db.models.signals import post_delete, post_save
from django.dispatch import receiver from django.dispatch import receiver
from authentik.core.models import USER_PATH_SYSTEM_PREFIX, Token, TokenIntents, User, UserTypes from authentik.core.models import USER_PATH_SYSTEM_PREFIX, Token, TokenIntents, User, UserTypes
from authentik.events.middleware import audit_ignore
from authentik.sources.scim.models import SCIMSource from authentik.sources.scim.models import SCIMSource
USER_PATH_SOURCE_SCIM = USER_PATH_SYSTEM_PREFIX + "/sources/scim" USER_PATH_SOURCE_SCIM = USER_PATH_SYSTEM_PREFIX + "/sources/scim"
@receiver(pre_save, sender=SCIMSource) @receiver(post_save, sender=SCIMSource)
def scim_source_pre_save(sender: type[Model], instance: SCIMSource, **_): def scim_source_post_save(sender: type[Model], instance: SCIMSource, created: bool, **_):
"""Create service account before source is saved""" """Create service account before source is saved"""
# .service_account_identifier will auto-assign a primary key uuid to the source
# if none is set yet, just so we can get the identifier before we save
identifier = instance.service_account_identifier identifier = instance.service_account_identifier
user = User.objects.create( user, _ = User.objects.update_or_create(
username=identifier, username=identifier,
name=f"SCIM Source {instance.name} Service-Account", defaults={
type=UserTypes.INTERNAL_SERVICE_ACCOUNT, "name": f"SCIM Source {instance.name} Service-Account",
path=USER_PATH_SOURCE_SCIM, "type": UserTypes.INTERNAL_SERVICE_ACCOUNT,
"path": USER_PATH_SOURCE_SCIM,
},
) )
token = Token.objects.create( token, token_created = Token.objects.update_or_create(
user=user,
identifier=identifier, identifier=identifier,
intent=TokenIntents.INTENT_API, defaults={
expiring=False, "user": user,
managed=f"goauthentik.io/sources/scim/{instance.pk}", "intent": TokenIntents.INTENT_API,
"expiring": False,
"managed": f"goauthentik.io/sources/scim/{instance.pk}",
},
) )
instance.token = token if created or token_created:
with audit_ignore():
instance.token = token
instance.save()
@receiver(pre_delete, sender=SCIMSource) @receiver(post_delete, sender=SCIMSource)
def scim_source_pre_delete(sender: type[Model], instance: SCIMSource, **_): def scim_source_post_delete(sender: type[Model], instance: SCIMSource, **_):
"""Delete SCIM Source service account before deleting source""" """Delete SCIM Source service account after deleting source"""
Token.objects.filter(
identifier=instance.service_account_identifier, intent=TokenIntents.INTENT_API
).delete()
User.objects.filter( User.objects.filter(
username=instance.service_account_identifier, type=UserTypes.INTERNAL_SERVICE_ACCOUNT username=instance.service_account_identifier, type=UserTypes.INTERNAL_SERVICE_ACCOUNT
).delete() ).delete()

View File

@ -13,7 +13,7 @@ def migrate_configuration_stage(apps: Apps, schema_editor: BaseDatabaseSchemaEdi
for stage in AuthenticatorValidateStage.objects.using(db_alias).all(): for stage in AuthenticatorValidateStage.objects.using(db_alias).all():
if stage.configuration_stage: if stage.configuration_stage:
stage.configuration_stages.set([stage.configuration_stage]) stage.configuration_stages.using(db_alias).set([stage.configuration_stage])
stage.save() stage.save()

View File

@ -325,7 +325,7 @@ class AuthenticatorValidateStageView(ChallengeStageView):
serializer = SelectableStageSerializer( serializer = SelectableStageSerializer(
data={ data={
"pk": stage.pk, "pk": stage.pk,
"name": stage.friendly_name or stage.name, "name": getattr(stage, "friendly_name", stage.name),
"verbose_name": str(stage._meta.verbose_name) "verbose_name": str(stage._meta.verbose_name)
.replace("Setup Stage", "") .replace("Setup Stage", "")
.strip(), .strip(),

View File

@ -8,6 +8,7 @@ from django.urls import reverse
from rest_framework.exceptions import ValidationError from rest_framework.exceptions import ValidationError
from authentik.brands.utils import get_brand_for_request from authentik.brands.utils import get_brand_for_request
from authentik.core.middleware import RESPONSE_HEADER_ID
from authentik.core.tests.utils import create_test_admin_user, create_test_flow from authentik.core.tests.utils import create_test_admin_user, create_test_flow
from authentik.events.models import Event, EventAction from authentik.events.models import Event, EventAction
from authentik.flows.models import FlowDesignation, FlowStageBinding from authentik.flows.models import FlowDesignation, FlowStageBinding
@ -186,6 +187,7 @@ class AuthenticatorValidateStageDuoTests(FlowTestCase):
"method": "GET", "method": "GET",
"path": f"/api/v3/flows/executor/{flow.slug}/", "path": f"/api/v3/flows/executor/{flow.slug}/",
"user_agent": "", "user_agent": "",
"request_id": response[RESPONSE_HEADER_ID],
}, },
}, },
) )

View File

@ -120,7 +120,7 @@
</tr> </tr>
<tr> <tr>
<td align="center"> <td align="center">
Powered by <a href="https://goauthentik.io?utm_source=authentik&utm_medium=email">authentik</a>. Powered by <a rel="noopener noreferrer" target="_blank" href="https://goauthentik.io?utm_source=authentik&utm_medium=email">authentik</a>.
</td> </td>
</tr> </tr>
</table> </table>

View File

@ -13,9 +13,9 @@ def assign_sources(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
IdentificationStage = apps.get_model("authentik_stages_identification", "identificationstage") IdentificationStage = apps.get_model("authentik_stages_identification", "identificationstage")
Source = apps.get_model("authentik_core", "source") Source = apps.get_model("authentik_core", "source")
sources = Source.objects.all() sources = Source.objects.using(db_alias).all()
for stage in IdentificationStage.objects.all().using(db_alias): for stage in IdentificationStage.objects.using(db_alias).all():
stage.sources.set(sources) stage.sources.using(db_alias).set(sources)
stage.save() stage.save()
@ -144,7 +144,7 @@ class Migration(migrations.Migration):
default=None, default=None,
help_text=( help_text=(
"When set, shows a password field, instead of showing the password field as" "When set, shows a password field, instead of showing the password field as"
" seaprate step." " separate step."
), ),
null=True, null=True,
on_delete=django.db.models.deletion.SET_NULL, on_delete=django.db.models.deletion.SET_NULL,

View File

@ -108,7 +108,7 @@ class PromptViewSet(UsedByMixin, ModelViewSet):
return Response( return Response(
{ {
"non_field_errors": [ "non_field_errors": [
exception_to_string(exc), exception_to_string(exc.exc),
] ]
}, },
status=400, status=400,

View File

@ -12,7 +12,7 @@ def set_generated_name(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
for prompt in Prompt.objects.using(db_alias).all(): for prompt in Prompt.objects.using(db_alias).all():
name = prompt.field_key name = prompt.field_key
stage = prompt.promptstage_set.order_by("name").first() stage = prompt.promptstage_set.using(db_alias).order_by("name").first()
if stage: if stage:
name += "_" + stage.name name += "_" + stage.name
else: else:

View File

@ -170,7 +170,7 @@ class Prompt(SerializerModel):
try: try:
raw_choices = evaluator.evaluate(self.placeholder) raw_choices = evaluator.evaluate(self.placeholder)
except Exception as exc: # pylint:disable=broad-except except Exception as exc: # pylint:disable=broad-except
wrapped = PropertyMappingExpressionException(str(exc)) wrapped = PropertyMappingExpressionException(exc, None)
LOGGER.warning( LOGGER.warning(
"failed to evaluate prompt choices", "failed to evaluate prompt choices",
exc=wrapped, exc=wrapped,
@ -208,7 +208,7 @@ class Prompt(SerializerModel):
try: try:
return evaluator.evaluate(self.placeholder) return evaluator.evaluate(self.placeholder)
except Exception as exc: # pylint:disable=broad-except except Exception as exc: # pylint:disable=broad-except
wrapped = PropertyMappingExpressionException(str(exc), None) wrapped = PropertyMappingExpressionException(exc, None)
LOGGER.warning( LOGGER.warning(
"failed to evaluate prompt placeholder", "failed to evaluate prompt placeholder",
exc=wrapped, exc=wrapped,
@ -237,7 +237,7 @@ class Prompt(SerializerModel):
try: try:
value = evaluator.evaluate(self.initial_value) value = evaluator.evaluate(self.initial_value)
except Exception as exc: # pylint:disable=broad-except except Exception as exc: # pylint:disable=broad-except
wrapped = PropertyMappingExpressionException(str(exc)) wrapped = PropertyMappingExpressionException(exc, None)
LOGGER.warning( LOGGER.warning(
"failed to evaluate prompt initial value", "failed to evaluate prompt initial value",
exc=wrapped, exc=wrapped,

View File

@ -1,10 +1,9 @@
"""Sessions bound to ASN/Network and GeoIP/Continent/etc""" """Sessions bound to ASN/Network and GeoIP/Continent/etc"""
from django.conf import settings
from django.contrib.auth.middleware import AuthenticationMiddleware from django.contrib.auth.middleware import AuthenticationMiddleware
from django.contrib.auth.signals import user_logged_out from django.contrib.auth.signals import user_logged_out
from django.contrib.auth.views import redirect_to_login
from django.http.request import HttpRequest from django.http.request import HttpRequest
from django.shortcuts import redirect
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from authentik.core.models import AuthenticatedSession from authentik.core.models import AuthenticatedSession
@ -87,7 +86,7 @@ class BoundSessionMiddleware(SessionMiddleware):
AuthenticationMiddleware(lambda request: request).process_request(request) AuthenticationMiddleware(lambda request: request).process_request(request)
logout_extra(request, exc) logout_extra(request, exc)
request.session.clear() request.session.clear()
return redirect(settings.LOGIN_URL) return redirect_to_login(request.get_full_path())
return None return None
def recheck_session(self, request: HttpRequest): def recheck_session(self, request: HttpRequest):

View File

@ -6,6 +6,7 @@ from django.contrib.auth import update_session_auth_hash
from django.db import transaction from django.db import transaction
from django.db.utils import IntegrityError, InternalError from django.db.utils import IntegrityError, InternalError
from django.http import HttpRequest, HttpResponse from django.http import HttpRequest, HttpResponse
from django.utils.functional import SimpleLazyObject
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from rest_framework.exceptions import ValidationError from rest_framework.exceptions import ValidationError
@ -118,6 +119,14 @@ class UserWriteStageView(StageView):
UserWriteStageView.write_attribute(user, key, value) UserWriteStageView.write_attribute(user, key, value)
# User has this key already # User has this key already
elif hasattr(user, key): elif hasattr(user, key):
if isinstance(user, SimpleLazyObject):
user._setup()
user = user._wrapped
attr = getattr(type(user), key)
if isinstance(attr, property):
if not attr.fset:
self.logger.info("discarding key", key=key)
continue
setattr(user, key, value) setattr(user, key, value)
# If none of the cases above matched, we have an attribute that the user doesn't have, # If none of the cases above matched, we have an attribute that the user doesn't have,
# has no setter for, is not a nested attributes value and as such is invalid # has no setter for, is not a nested attributes value and as such is invalid

View File

@ -2,7 +2,7 @@
"$schema": "http://json-schema.org/draft-07/schema", "$schema": "http://json-schema.org/draft-07/schema",
"$id": "https://goauthentik.io/blueprints/schema.json", "$id": "https://goauthentik.io/blueprints/schema.json",
"type": "object", "type": "object",
"title": "authentik 2024.6.0 Blueprint schema", "title": "authentik 2024.6.3 Blueprint schema",
"required": [ "required": [
"version", "version",
"entries" "entries"

View File

@ -31,7 +31,7 @@ services:
volumes: volumes:
- redis:/data - redis:/data
server: server:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.6.0} image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.6.3}
restart: unless-stopped restart: unless-stopped
command: server command: server
environment: environment:
@ -52,7 +52,7 @@ services:
- postgresql - postgresql
- redis - redis
worker: worker:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.6.0} image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.6.3}
restart: unless-stopped restart: unless-stopped
command: worker command: worker
environment: environment:

2
go.mod
View File

@ -28,7 +28,7 @@ require (
github.com/spf13/cobra v1.8.0 github.com/spf13/cobra v1.8.0
github.com/stretchr/testify v1.9.0 github.com/stretchr/testify v1.9.0
github.com/wwt/guac v1.3.2 github.com/wwt/guac v1.3.2
goauthentik.io/api/v3 v3.2024042.11 goauthentik.io/api/v3 v3.2024060.5
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
golang.org/x/oauth2 v0.21.0 golang.org/x/oauth2 v0.21.0
golang.org/x/sync v0.7.0 golang.org/x/sync v0.7.0

4
go.sum
View File

@ -294,8 +294,8 @@ go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4=
goauthentik.io/api/v3 v3.2024042.11 h1:cGgUz1E8rlMphGvv04VI7i+MgT8eidZbxTpza5zd96I= goauthentik.io/api/v3 v3.2024060.5 h1:AjvPUZoObk7a86ZZaz2tmruteY+1vAEfVzIOzQpWSXM=
goauthentik.io/api/v3 v3.2024042.11/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw= goauthentik.io/api/v3 v3.2024060.5/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=

View File

@ -29,4 +29,4 @@ func UserAgent() string {
return fmt.Sprintf("authentik@%s", FullVersion()) return fmt.Sprintf("authentik@%s", FullVersion())
} }
const VERSION = "2024.6.0" const VERSION = "2024.6.3"

View File

@ -183,7 +183,19 @@ func (ac *APIController) startWSHealth() {
func (ac *APIController) startIntervalUpdater() { func (ac *APIController) startIntervalUpdater() {
logger := ac.logger.WithField("loop", "interval-updater") logger := ac.logger.WithField("loop", "interval-updater")
ticker := time.NewTicker(5 * time.Minute) getInterval := func() time.Duration {
// Ensure timer interval is not negative or 0
// for 0 we assume migration or unconfigured, so default to 5 minutes
if ac.Outpost.RefreshIntervalS <= 0 {
return 5 * time.Minute
}
// Clamp interval to be at least 30 seconds
if ac.Outpost.RefreshIntervalS < 30 {
return 30 * time.Second
}
return time.Duration(ac.Outpost.RefreshIntervalS) * time.Second
}
ticker := time.NewTicker(getInterval())
for ; true; <-ticker.C { for ; true; <-ticker.C {
logger.Debug("Running interval update") logger.Debug("Running interval update")
err := ac.OnRefresh() err := ac.OnRefresh()
@ -198,6 +210,7 @@ func (ac *APIController) startIntervalUpdater() {
"build": constants.BUILD("tagged"), "build": constants.BUILD("tagged"),
}).SetToCurrentTime() }).SetToCurrentTime()
} }
ticker.Reset(getInterval())
} }
} }

View File

@ -48,9 +48,9 @@
<footer class="pf-c-login__footer"> <footer class="pf-c-login__footer">
<ul class="pf-c-list pf-m-inline"> <ul class="pf-c-list pf-m-inline">
<li> <li>
<a href="https://goauthentik.io?utm_source=authentik_outpost&utm_campaign=proxy_error"> <span>
Powered by authentik Powered by authentik
</a> </span>
</li> </li>
</ul> </ul>
</footer> </footer>

View File

@ -1,6 +1,7 @@
# flake8: noqa # flake8: noqa
from pathlib import Path from pathlib import Path
from authentik.lib.config import CONFIG
from lifecycle.migrate import BaseMigration from lifecycle.migrate import BaseMigration
MEDIA_ROOT = Path(__file__).parent.parent.parent / "media" MEDIA_ROOT = Path(__file__).parent.parent.parent / "media"
@ -9,7 +10,9 @@ TENANT_MEDIA_ROOT = MEDIA_ROOT / "public"
class Migration(BaseMigration): class Migration(BaseMigration):
def needs_migration(self) -> bool: def needs_migration(self) -> bool:
return not TENANT_MEDIA_ROOT.exists() return (
not TENANT_MEDIA_ROOT.exists() and CONFIG.get("storage.media.backend", "file") != "s3"
)
def run(self): def run(self):
TENANT_MEDIA_ROOT.mkdir(parents=True) TENANT_MEDIA_ROOT.mkdir(parents=True)

View File

@ -1,5 +1,5 @@
{ {
"name": "@goauthentik/authentik", "name": "@goauthentik/authentik",
"version": "2024.6.0", "version": "2024.6.3",
"private": true "private": true
} }

29
poetry.lock generated
View File

@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. # This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
[[package]] [[package]]
name = "aiohttp" name = "aiohttp"
@ -1513,20 +1513,6 @@ files = [
dnspython = ">=2.0.0" dnspython = ">=2.0.0"
idna = ">=2.0.0" idna = ">=2.0.0"
[[package]]
name = "facebook-sdk"
version = "3.1.0"
description = "This client library is designed to support the Facebook Graph API and the official Facebook JavaScript SDK, which is the canonical way to implement Facebook authentication."
optional = false
python-versions = "*"
files = [
{file = "facebook-sdk-3.1.0.tar.gz", hash = "sha256:cabcd2e69ea3d9f042919c99b353df7aa1e2be86d040121f6e9f5e63c1cf0f8d"},
{file = "facebook_sdk-3.1.0-py2.py3-none-any.whl", hash = "sha256:2e987b3e0f466a6f4ee77b935eb023dba1384134f004a2af21f1cfff7fe0806e"},
]
[package.dependencies]
requests = "*"
[[package]] [[package]]
name = "fido2" name = "fido2"
version = "1.1.3" version = "1.1.3"
@ -2954,9 +2940,14 @@ version = "0.0.14"
description = "Python module for oci specifications" description = "Python module for oci specifications"
optional = false optional = false
python-versions = "*" python-versions = "*"
files = [ files = []
{file = "opencontainers-0.0.14.tar.gz", hash = "sha256:fde3b8099b56b5c956415df8933e2227e1914e805a277b844f2f9e52341738f2"}, develop = false
]
[package.source]
type = "git"
url = "https://github.com/vsoch/oci-python"
reference = "20d69d9cc50a0fef31605b46f06da0c94f1ec3cf"
resolved_reference = "20d69d9cc50a0fef31605b46f06da0c94f1ec3cf"
[[package]] [[package]]
name = "opentelemetry-api" name = "opentelemetry-api"
@ -5350,4 +5341,4 @@ files = [
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "~3.12" python-versions = "~3.12"
content-hash = "f960013b56683ab42d82f8b49b2822dffc76046e3d22695ebb737b405a98dbaf" content-hash = "055376879ff784080ab95c02eaa012fb1dad1213b1faa0dd1d61b0b812859b6d"

View File

@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "authentik" name = "authentik"
version = "2024.6.0" version = "2024.6.3"
description = "" description = ""
authors = ["authentik Team <hello@goauthentik.io>"] authors = ["authentik Team <hello@goauthentik.io>"]
@ -110,7 +110,6 @@ docker = "*"
drf-spectacular = "*" drf-spectacular = "*"
dumb-init = "*" dumb-init = "*"
duo-client = "*" duo-client = "*"
facebook-sdk = "*"
fido2 = "*" fido2 = "*"
flower = "*" flower = "*"
geoip2 = "*" geoip2 = "*"
@ -121,7 +120,7 @@ kubernetes = "*"
ldap3 = "*" ldap3 = "*"
lxml = "*" lxml = "*"
msgraph-sdk = "*" msgraph-sdk = "*"
opencontainers = { extras = ["reggie"], version = "*" } opencontainers = { git = "https://github.com/vsoch/oci-python", rev = "20d69d9cc50a0fef31605b46f06da0c94f1ec3cf", extras = ["reggie"] }
packaging = "*" packaging = "*"
paramiko = "*" paramiko = "*"
psycopg = { extras = ["c"], version = "*" } psycopg = { extras = ["c"], version = "*" }

View File

@ -1,7 +1,7 @@
openapi: 3.0.3 openapi: 3.0.3
info: info:
title: authentik title: authentik
version: 2024.6.0 version: 2024.6.3
description: Making authentication simple. description: Making authentication simple.
contact: contact:
email: hello@goauthentik.io email: hello@goauthentik.io
@ -13080,6 +13080,15 @@ paths:
name: identifier name: identifier
schema: schema:
type: string type: string
- in: query
name: identifier_in
schema:
type: array
items:
type: string
description: Multiple values may be separated by commas.
explode: false
style: form
- in: query - in: query
name: ip name: ip
schema: schema:
@ -36625,6 +36634,7 @@ components:
href: href:
type: string type: string
readOnly: true readOnly: true
nullable: true
name: name:
type: string type: string
readOnly: true readOnly: true
@ -39488,6 +39498,9 @@ components:
allOf: allOf:
- $ref: '#/components/schemas/ServiceConnection' - $ref: '#/components/schemas/ServiceConnection'
readOnly: true readOnly: true
refresh_interval_s:
type: integer
readOnly: true
token_identifier: token_identifier:
type: string type: string
description: Get Token identifier description: Get Token identifier
@ -39509,6 +39522,7 @@ components:
- pk - pk
- providers - providers
- providers_obj - providers_obj
- refresh_interval_s
- service_connection_obj - service_connection_obj
- token_identifier - token_identifier
- type - type

View File

@ -0,0 +1,23 @@
<?php
/**
* SAML 2.0 remote SP metadata for SimpleSAMLphp.
*
* See: https://simplesamlphp.org/docs/stable/simplesamlphp-reference-sp-remote
*/
$metadata[getenv('SIMPLESAMLPHP_SP_ENTITY_ID')] = array(
'AssertionConsumerService' => getenv('SIMPLESAMLPHP_SP_ASSERTION_CONSUMER_SERVICE'),
'SingleLogoutService' => getenv('SIMPLESAMLPHP_SP_SINGLE_LOGOUT_SERVICE'),
);
if (null != getenv('SIMPLESAMLPHP_SP_NAME_ID_FORMAT')) {
$metadata[getenv('SIMPLESAMLPHP_SP_ENTITY_ID')] = array_merge($metadata[getenv('SIMPLESAMLPHP_SP_ENTITY_ID')], array('NameIDFormat' => getenv('SIMPLESAMLPHP_SP_NAME_ID_FORMAT')));
}
if (null != getenv('SIMPLESAMLPHP_SP_NAME_ID_ATTRIBUTE')) {
$metadata[getenv('SIMPLESAMLPHP_SP_ENTITY_ID')] = array_merge($metadata[getenv('SIMPLESAMLPHP_SP_ENTITY_ID')], array('simplesaml.nameidattribute' => getenv('SIMPLESAMLPHP_SP_NAME_ID_ATTRIBUTE')));
}
if (null != getenv('SIMPLESAMLPHP_SP_SIGN_ASSERTION')) {
$metadata[getenv('SIMPLESAMLPHP_SP_ENTITY_ID')] = array_merge($metadata[getenv('SIMPLESAMLPHP_SP_ENTITY_ID')], array('saml20.sign.assertion' => ('true' == getenv('SIMPLESAMLPHP_SP_SIGN_ASSERTION'))));
}

View File

@ -5,7 +5,6 @@ from time import sleep
from docker.client import DockerClient, from_env from docker.client import DockerClient, from_env
from docker.models.containers import Container from docker.models.containers import Container
from guardian.shortcuts import get_anonymous_user
from ldap3 import ALL, ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES, SUBTREE, Connection, Server from ldap3 import ALL, ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES, SUBTREE, Connection, Server
from ldap3.core.exceptions import LDAPInvalidCredentialsResult from ldap3.core.exceptions import LDAPInvalidCredentialsResult
@ -180,15 +179,13 @@ class TestProviderLDAP(SeleniumTestCase):
) )
with self.assertRaises(LDAPInvalidCredentialsResult): with self.assertRaises(LDAPInvalidCredentialsResult):
_connection.bind() _connection.bind()
anon = get_anonymous_user()
self.assertTrue( self.assertTrue(
Event.objects.filter( Event.objects.filter(
action=EventAction.LOGIN_FAILED, action=EventAction.LOGIN_FAILED,
user={ user={
"pk": anon.pk, "pk": self.user.pk,
"email": anon.email, "email": self.user.email,
"username": anon.username, "username": self.user.username,
"is_anonymous": True,
}, },
).exists(), ).exists(),
) )

View File

@ -1,5 +1,6 @@
"""test OAuth Source""" """test OAuth Source"""
from json import loads
from pathlib import Path from pathlib import Path
from time import sleep from time import sleep
from typing import Any from typing import Any
@ -194,3 +195,41 @@ class TestSourceOAuth2(SeleniumTestCase):
self.driver.get(self.if_user_url("/settings")) self.driver.get(self.if_user_url("/settings"))
self.assert_user(User(username="foo", name="admin", email="admin@example.com")) self.assert_user(User(username="foo", name="admin", email="admin@example.com"))
@retry()
@apply_blueprint(
"default/flow-default-authentication-flow.yaml",
"default/flow-default-invalidation-flow.yaml",
)
@apply_blueprint(
"default/flow-default-source-authentication.yaml",
"default/flow-default-source-enrollment.yaml",
"default/flow-default-source-pre-authentication.yaml",
)
def test_oauth_link(self):
"""test OAuth Source link OIDC"""
self.create_objects()
self.driver.get(self.live_server_url)
self.login()
self.driver.get(
self.url("authentik_sources_oauth:oauth-client-login", source_slug=self.slug)
)
# Now we should be at the IDP, wait for the login field
self.wait.until(ec.presence_of_element_located((By.ID, "login")))
self.driver.find_element(By.ID, "login").send_keys("admin@example.com")
self.driver.find_element(By.ID, "password").send_keys("password")
self.driver.find_element(By.ID, "password").send_keys(Keys.ENTER)
# Wait until we're logged in
self.wait.until(ec.presence_of_element_located((By.CSS_SELECTOR, "button[type=submit]")))
self.driver.find_element(By.CSS_SELECTOR, "button[type=submit]").click()
self.driver.get(self.url("authentik_api:usersourceconnection-list") + "?format=json")
body_json = loads(self.driver.find_element(By.CSS_SELECTOR, "pre").text)
results = body_json["results"]
self.assertEqual(len(results), 1)
connection = results[0]
self.assertEqual(connection["source"]["slug"], self.slug)
self.assertEqual(connection["user"], self.user.pk)

View File

@ -1,5 +1,6 @@
"""test SAML Source""" """test SAML Source"""
from pathlib import Path
from time import sleep from time import sleep
from typing import Any from typing import Any
@ -88,8 +89,20 @@ class TestSourceSAML(SeleniumTestCase):
interval=5 * 1_000 * 1_000_000, interval=5 * 1_000 * 1_000_000,
start_period=1 * 1_000 * 1_000_000, start_period=1 * 1_000 * 1_000_000,
), ),
"volumes": {
str(
(Path(__file__).parent / Path("test-saml-idp/saml20-sp-remote.php")).absolute()
): {
"bind": "/var/www/simplesamlphp/metadata/saml20-sp-remote.php",
"mode": "ro",
}
},
"environment": { "environment": {
"SIMPLESAMLPHP_SP_ENTITY_ID": "entity-id", "SIMPLESAMLPHP_SP_ENTITY_ID": "entity-id",
"SIMPLESAMLPHP_SP_NAME_ID_FORMAT": (
"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
),
"SIMPLESAMLPHP_SP_NAME_ID_ATTRIBUTE": "email",
"SIMPLESAMLPHP_SP_ASSERTION_CONSUMER_SERVICE": ( "SIMPLESAMLPHP_SP_ASSERTION_CONSUMER_SERVICE": (
self.url("authentik_sources_saml:acs", source_slug=self.slug) self.url("authentik_sources_saml:acs", source_slug=self.slug)
), ),
@ -318,3 +331,109 @@ class TestSourceSAML(SeleniumTestCase):
.exclude(pk=self.user.pk) .exclude(pk=self.user.pk)
.first() .first()
) )
@retry()
@apply_blueprint(
"default/flow-default-authentication-flow.yaml",
"default/flow-default-invalidation-flow.yaml",
)
@apply_blueprint(
"default/flow-default-source-authentication.yaml",
"default/flow-default-source-enrollment.yaml",
"default/flow-default-source-pre-authentication.yaml",
)
def test_idp_post_auto_enroll_auth(self):
"""test SAML Source With post binding (auto redirect)"""
# Bootstrap all needed objects
authentication_flow = Flow.objects.get(slug="default-source-authentication")
enrollment_flow = Flow.objects.get(slug="default-source-enrollment")
pre_authentication_flow = Flow.objects.get(slug="default-source-pre-authentication")
keypair = CertificateKeyPair.objects.create(
name=generate_id(),
certificate_data=IDP_CERT,
key_data=IDP_KEY,
)
source = SAMLSource.objects.create(
name=generate_id(),
slug=self.slug,
authentication_flow=authentication_flow,
enrollment_flow=enrollment_flow,
pre_authentication_flow=pre_authentication_flow,
issuer="entity-id",
sso_url=f"http://{self.host}:8080/simplesaml/saml2/idp/SSOService.php",
binding_type=SAMLBindingTypes.POST_AUTO,
signing_kp=keypair,
)
ident_stage = IdentificationStage.objects.first()
ident_stage.sources.set([source])
ident_stage.save()
self.driver.get(self.live_server_url)
flow_executor = self.get_shadow_root("ak-flow-executor")
identification_stage = self.get_shadow_root("ak-stage-identification", flow_executor)
wait = WebDriverWait(identification_stage, self.wait_timeout)
wait.until(
ec.presence_of_element_located(
(By.CSS_SELECTOR, ".pf-c-login__main-footer-links-item > button")
)
)
identification_stage.find_element(
By.CSS_SELECTOR, ".pf-c-login__main-footer-links-item > button"
).click()
# Now we should be at the IDP, wait for the username field
self.wait.until(ec.presence_of_element_located((By.ID, "username")))
self.driver.find_element(By.ID, "username").send_keys("user1")
self.driver.find_element(By.ID, "password").send_keys("user1pass")
self.driver.find_element(By.ID, "password").send_keys(Keys.ENTER)
# Wait until we're logged in
self.wait_for_url(self.if_user_url("/library"))
self.driver.get(self.if_user_url("/settings"))
self.assert_user(
User.objects.exclude(username="akadmin")
.exclude(username__startswith="ak-outpost")
.exclude_anonymous()
.exclude(pk=self.user.pk)
.first()
)
# Clear all cookies and log in again
self.driver.delete_all_cookies()
self.driver.get(self.live_server_url)
flow_executor = self.get_shadow_root("ak-flow-executor")
identification_stage = self.get_shadow_root("ak-stage-identification", flow_executor)
wait = WebDriverWait(identification_stage, self.wait_timeout)
wait.until(
ec.presence_of_element_located(
(By.CSS_SELECTOR, ".pf-c-login__main-footer-links-item > button")
)
)
identification_stage.find_element(
By.CSS_SELECTOR, ".pf-c-login__main-footer-links-item > button"
).click()
# Now we should be at the IDP, wait for the username field
self.wait.until(ec.presence_of_element_located((By.ID, "username")))
self.driver.find_element(By.ID, "username").send_keys("user1")
self.driver.find_element(By.ID, "password").send_keys("user1pass")
self.driver.find_element(By.ID, "password").send_keys(Keys.ENTER)
# Wait until we're logged in
self.wait_for_url(self.if_user_url("/library"))
self.driver.get(self.if_user_url("/settings"))
# sleep(999999)
self.assert_user(
User.objects.exclude(username="akadmin")
.exclude(username__startswith="ak-outpost")
.exclude_anonymous()
.exclude(pk=self.user.pk)
.first()
)

7823
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -38,7 +38,7 @@
"@codemirror/theme-one-dark": "^6.1.2", "@codemirror/theme-one-dark": "^6.1.2",
"@formatjs/intl-listformat": "^7.5.7", "@formatjs/intl-listformat": "^7.5.7",
"@fortawesome/fontawesome-free": "^6.5.2", "@fortawesome/fontawesome-free": "^6.5.2",
"@goauthentik/api": "^2024.4.2-1718378698", "@goauthentik/api": "^2024.6.0-1719577139",
"@lit/context": "^1.1.2", "@lit/context": "^1.1.2",
"@lit/localize": "^0.12.1", "@lit/localize": "^0.12.1",
"@lit/reactive-element": "^2.0.4", "@lit/reactive-element": "^2.0.4",

529
web/sfe/index.ts Normal file
View File

@ -0,0 +1,529 @@
import { fromByteArray } from "base64-js";
import "formdata-polyfill";
import $ from "jquery";
import "weakmap-polyfill";
import {
type AuthenticatorValidationChallenge,
type AutosubmitChallenge,
type ChallengeTypes,
ChallengeTypesFromJSON,
type ContextualFlowInfo,
type DeviceChallenge,
type ErrorDetail,
type IdentificationChallenge,
type PasswordChallenge,
type RedirectChallenge,
} from "@goauthentik/api";
interface GlobalAuthentik {
brand: {
branding_logo: string;
};
}
function ak(): GlobalAuthentik {
return (
window as unknown as {
authentik: GlobalAuthentik;
}
).authentik;
}
class SimpleFlowExecutor {
challenge?: ChallengeTypes;
flowSlug: string;
container: HTMLDivElement;
constructor(container: HTMLDivElement) {
this.flowSlug = window.location.pathname.split("/")[3];
this.container = container;
}
get apiURL() {
return `/api/v3/flows/executor/${this.flowSlug}/?query=${encodeURIComponent(window.location.search.substring(1))}`;
}
start() {
$.ajax({
type: "GET",
url: this.apiURL,
success: (data) => {
this.challenge = ChallengeTypesFromJSON(data);
this.renderChallenge();
},
});
}
submit(data: { [key: string]: unknown } | FormData) {
$("button[type=submit]").addClass("disabled")
.html(`<span class="spinner-border spinner-border-sm" aria-hidden="true"></span>
<span role="status">Loading...</span>`);
let finalData: { [key: string]: unknown } = {};
if (data instanceof FormData) {
finalData = {};
data.forEach((value, key) => {
finalData[key] = value;
});
} else {
finalData = data;
}
$.ajax({
type: "POST",
url: this.apiURL,
data: JSON.stringify(finalData),
success: (data) => {
this.challenge = ChallengeTypesFromJSON(data);
this.renderChallenge();
},
contentType: "application/json",
dataType: "json",
});
}
renderChallenge() {
switch (this.challenge?.component) {
case "ak-stage-identification":
new IdentificationStage(this, this.challenge).render();
return;
case "ak-stage-password":
new PasswordStage(this, this.challenge).render();
return;
case "xak-flow-redirect":
new RedirectStage(this, this.challenge).render();
return;
case "ak-stage-autosubmit":
new AutosubmitStage(this, this.challenge).render();
return;
case "ak-stage-authenticator-validate":
new AuthenticatorValidateStage(this, this.challenge).render();
return;
default:
this.container.innerText = "Unsupported stage: " + this.challenge?.component;
return;
}
}
}
export interface FlowInfoChallenge {
flowInfo?: ContextualFlowInfo;
responseErrors?: {
[key: string]: Array<ErrorDetail>;
};
}
class Stage<T extends FlowInfoChallenge> {
constructor(
public executor: SimpleFlowExecutor,
public challenge: T,
) {}
error(fieldName: string) {
if (!this.challenge.responseErrors) {
return [];
}
return this.challenge.responseErrors[fieldName] || [];
}
renderInputError(fieldName: string) {
return `${this.error(fieldName)
.map((error) => {
return `<div class="invalid-feedback">
${error.string}
</div>`;
})
.join("")}`;
}
renderNonFieldErrors() {
return `${this.error("non_field_errors")
.map((error) => {
return `<div class="alert alert-danger" role="alert">
${error.string}
</div>`;
})
.join("")}`;
}
html(html: string) {
this.executor.container.innerHTML = html;
}
render() {
throw new Error("Abstract method");
}
}
class IdentificationStage extends Stage<IdentificationChallenge> {
render() {
this.html(`
<form id="ident-form">
<img class="mb-4 brand-icon" src="${ak().brand.branding_logo}" alt="">
<h1 class="h3 mb-3 fw-normal text-center">${this.challenge?.flowInfo?.title}</h1>
${
this.challenge.applicationPre
? `<p>
Login to continue to ${this.challenge.applicationPre}.
</p>`
: ""
}
<div class="form-label-group my-3 has-validation">
<input type="text" autofocus class="form-control" name="uid_field" placeholder="Email / Username">
</div>
${
this.challenge.passwordFields
? `<div class="form-label-group my-3 has-validation">
<input type="password" class="form-control ${this.error("password").length > 0 ? "is-invalid" : ""}" name="password" placeholder="Password">
${this.renderInputError("password")}
</div>`
: ""
}
${this.renderNonFieldErrors()}
<button class="btn btn-primary w-100 py-2" type="submit">${this.challenge.primaryAction}</button>
</form>`);
$("#ident-form input[name=uid_field]").trigger("focus");
$("#ident-form").on("submit", (ev) => {
ev.preventDefault();
const data = new FormData(ev.target as HTMLFormElement);
this.executor.submit(data);
});
}
}
class PasswordStage extends Stage<PasswordChallenge> {
render() {
this.html(`
<form id="password-form">
<img class="mb-4 brand-icon" src="${ak().brand.branding_logo}" alt="">
<h1 class="h3 mb-3 fw-normal text-center">${this.challenge?.flowInfo?.title}</h1>
<div class="form-label-group my-3 has-validation">
<input type="password" autofocus class="form-control ${this.error("password").length > 0 ? "is-invalid" : ""}" name="password" placeholder="Password">
${this.renderInputError("password")}
</div>
<button class="btn btn-primary w-100 py-2" type="submit">Continue</button>
</form>`);
$("#password-form input").trigger("focus");
$("#password-form").on("submit", (ev) => {
ev.preventDefault();
const data = new FormData(ev.target as HTMLFormElement);
this.executor.submit(data);
});
}
}
class RedirectStage extends Stage<RedirectChallenge> {
render() {
window.location.assign(this.challenge.to);
}
}
class AutosubmitStage extends Stage<AutosubmitChallenge> {
render() {
this.html(`
<form id="autosubmit-form" action="${this.challenge.url}" method="POST">
<img class="mb-4 brand-icon" src="${ak().brand.branding_logo}" alt="">
<h1 class="h3 mb-3 fw-normal text-center">${this.challenge?.flowInfo?.title}</h1>
${Object.entries(this.challenge.attrs).map(([key, value]) => {
return `<input
type="hidden"
name="${key}"
value="${value}"
/>`;
})}
<div class="d-flex justify-content-center">
<div class="spinner-border" role="status">
<span class="sr-only">Loading...</span>
</div>
</div>
</form>`);
$("#autosubmit-form").submit();
}
}
export interface Assertion {
id: string;
rawId: string;
type: string;
registrationClientExtensions: string;
response: {
clientDataJSON: string;
attestationObject: string;
};
}
export interface AuthAssertion {
id: string;
rawId: string;
type: string;
assertionClientExtensions: string;
response: {
clientDataJSON: string;
authenticatorData: string;
signature: string;
userHandle: string | null;
};
}
class AuthenticatorValidateStage extends Stage<AuthenticatorValidationChallenge> {
deviceChallenge?: DeviceChallenge;
b64enc(buf: Uint8Array): string {
return fromByteArray(buf).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
}
b64RawEnc(buf: Uint8Array): string {
return fromByteArray(buf).replace(/\+/g, "-").replace(/\//g, "_");
}
u8arr(input: string): Uint8Array {
return Uint8Array.from(atob(input.replace(/_/g, "/").replace(/-/g, "+")), (c) =>
c.charCodeAt(0),
);
}
checkWebAuthnSupport(): boolean {
if ("credentials" in navigator) {
return true;
}
if (window.location.protocol === "http:" && window.location.hostname !== "localhost") {
console.warn("WebAuthn requires this page to be accessed via HTTPS.");
return false;
}
console.warn("WebAuthn not supported by browser.");
return false;
}
/**
* Transforms items in the credentialCreateOptions generated on the server
* into byte arrays expected by the navigator.credentials.create() call
*/
transformCredentialCreateOptions(
credentialCreateOptions: PublicKeyCredentialCreationOptions,
userId: string,
): PublicKeyCredentialCreationOptions {
const user = credentialCreateOptions.user;
// Because json can't contain raw bytes, the server base64-encodes the User ID
// So to get the base64 encoded byte array, we first need to convert it to a regular
// string, then a byte array, re-encode it and wrap that in an array.
const stringId = decodeURIComponent(window.atob(userId));
user.id = this.u8arr(this.b64enc(this.u8arr(stringId)));
const challenge = this.u8arr(credentialCreateOptions.challenge.toString());
const transformedCredentialCreateOptions = Object.assign({}, credentialCreateOptions, {
challenge,
user,
});
return transformedCredentialCreateOptions;
}
/**
* Transforms the binary data in the credential into base64 strings
* for posting to the server.
* @param {PublicKeyCredential} newAssertion
*/
transformNewAssertionForServer(newAssertion: PublicKeyCredential): Assertion {
const attObj = new Uint8Array(
(newAssertion.response as AuthenticatorAttestationResponse).attestationObject,
);
const clientDataJSON = new Uint8Array(newAssertion.response.clientDataJSON);
const rawId = new Uint8Array(newAssertion.rawId);
const registrationClientExtensions = newAssertion.getClientExtensionResults();
return {
id: newAssertion.id,
rawId: this.b64enc(rawId),
type: newAssertion.type,
registrationClientExtensions: JSON.stringify(registrationClientExtensions),
response: {
clientDataJSON: this.b64enc(clientDataJSON),
attestationObject: this.b64enc(attObj),
},
};
}
transformCredentialRequestOptions(
credentialRequestOptions: PublicKeyCredentialRequestOptions,
): PublicKeyCredentialRequestOptions {
const challenge = this.u8arr(credentialRequestOptions.challenge.toString());
const allowCredentials = (credentialRequestOptions.allowCredentials || []).map(
(credentialDescriptor) => {
const id = this.u8arr(credentialDescriptor.id.toString());
return Object.assign({}, credentialDescriptor, { id });
},
);
const transformedCredentialRequestOptions = Object.assign({}, credentialRequestOptions, {
challenge,
allowCredentials,
});
return transformedCredentialRequestOptions;
}
/**
* Encodes the binary data in the assertion into strings for posting to the server.
* @param {PublicKeyCredential} newAssertion
*/
transformAssertionForServer(newAssertion: PublicKeyCredential): AuthAssertion {
const response = newAssertion.response as AuthenticatorAssertionResponse;
const authData = new Uint8Array(response.authenticatorData);
const clientDataJSON = new Uint8Array(response.clientDataJSON);
const rawId = new Uint8Array(newAssertion.rawId);
const sig = new Uint8Array(response.signature);
const assertionClientExtensions = newAssertion.getClientExtensionResults();
return {
id: newAssertion.id,
rawId: this.b64enc(rawId),
type: newAssertion.type,
assertionClientExtensions: JSON.stringify(assertionClientExtensions),
response: {
clientDataJSON: this.b64RawEnc(clientDataJSON),
signature: this.b64RawEnc(sig),
authenticatorData: this.b64RawEnc(authData),
userHandle: null,
},
};
}
render() {
if (!this.deviceChallenge) {
return this.renderChallengePicker();
}
switch (this.deviceChallenge.deviceClass) {
case "static":
case "totp":
this.renderCodeInput();
break;
case "webauthn":
this.renderWebauthn();
break;
default:
break;
}
}
renderChallengePicker() {
const challenges = this.challenge.deviceChallenges.filter((challenge) => {
if (challenge.deviceClass === "webauthn") {
if (!this.checkWebAuthnSupport()) {
return undefined;
}
}
return challenge;
});
this.html(`<form id="picker-form">
<img class="mb-4 brand-icon" src="${ak().brand.branding_logo}" alt="">
<h1 class="h3 mb-3 fw-normal text-center">${this.challenge?.flowInfo?.title}</h1>
${
challenges.length > 0
? "<p>Select an authentication method.</p>"
: `
<p>No compatible authentication method available</p>
`
}
${challenges
.map((challenge) => {
let label = undefined;
switch (challenge.deviceClass) {
case "static":
label = "Recovery keys";
break;
case "totp":
label = "Traditional authenticator";
break;
case "webauthn":
label = "Security key";
break;
}
if (!label) {
return "";
}
return `<div class="form-label-group my-3 has-validation">
<button id="${challenge.deviceClass}-${challenge.deviceUid}" class="btn btn-secondary w-100 py-2" type="button">
${label}
</button>
</div>`;
})
.join("")}
</form>`);
this.challenge.deviceChallenges.forEach((challenge) => {
$(`#picker-form button#${challenge.deviceClass}-${challenge.deviceUid}`).on(
"click",
() => {
this.deviceChallenge = challenge;
this.render();
},
);
});
}
renderCodeInput() {
this.html(`
<form id="totp-form">
<img class="mb-4 brand-icon" src="${ak().brand.branding_logo}" alt="">
<h1 class="h3 mb-3 fw-normal text-center">${this.challenge?.flowInfo?.title}</h1>
<div class="form-label-group my-3 has-validation">
<input type="text" autofocus class="form-control ${this.error("code").length > 0 ? "is-invalid" : ""}" name="code" placeholder="Please enter your code" autocomplete="one-time-code">
${this.renderInputError("code")}
</div>
<button class="btn btn-primary w-100 py-2" type="submit">Continue</button>
</form>`);
$("#totp-form input").trigger("focus");
$("#totp-form").on("submit", (ev) => {
ev.preventDefault();
const data = new FormData(ev.target as HTMLFormElement);
this.executor.submit(data);
});
}
renderWebauthn() {
this.html(`
<form id="totp-form">
<img class="mb-4 brand-icon" src="${ak().brand.branding_logo}" alt="">
<h1 class="h3 mb-3 fw-normal text-center">${this.challenge?.flowInfo?.title}</h1>
<div class="d-flex justify-content-center">
<div class="spinner-border" role="status">
<span class="sr-only">Loading...</span>
</div>
</div>
</form>
`);
navigator.credentials
.get({
publicKey: this.transformCredentialRequestOptions(
this.deviceChallenge?.challenge as PublicKeyCredentialRequestOptions,
),
})
.then((assertion) => {
if (!assertion) {
throw new Error("No assertion");
}
try {
// we now have an authentication assertion! encode the byte arrays contained
// in the assertion data as strings for posting to the server
const transformedAssertionForServer = this.transformAssertionForServer(
assertion as PublicKeyCredential,
);
// post the assertion to the server for verification.
this.executor.submit({
webauthn: transformedAssertionForServer,
});
} catch (err) {
throw new Error(`Error when validating assertion on server: ${err}`);
}
})
.catch((error) => {
console.warn(error);
this.deviceChallenge = undefined;
this.render();
});
}
}
const sfe = new SimpleFlowExecutor($("#flow-sfe-container")[0] as HTMLDivElement);
sfe.start();

3057
web/sfe/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

28
web/sfe/package.json Normal file
View File

@ -0,0 +1,28 @@
{
"name": "@goauthentik/web-sfe",
"version": "0.0.0",
"private": true,
"license": "MIT",
"dependencies": {
"@goauthentik/api": "^2024.6.0-1719577139",
"base64-js": "^1.5.1",
"bootstrap": "^4.6.1",
"formdata-polyfill": "^4.0.10",
"jquery": "^3.7.1",
"weakmap-polyfill": "^2.0.4"
},
"scripts": {
"build": "rollup -c rollup.config.js --bundleConfigAsCjs",
"watch": "rollup -w -c rollup.config.js --bundleConfigAsCjs"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^26.0.1",
"@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-swc": "^0.3.1",
"@swc/cli": "^0.3.14",
"@swc/core": "^1.6.6",
"@types/jquery": "^3.5.30",
"rollup": "^4.18.0",
"rollup-plugin-copy": "^3.5.0"
}
}

40
web/sfe/rollup.config.js Normal file
View File

@ -0,0 +1,40 @@
import commonjs from "@rollup/plugin-commonjs";
import resolve from "@rollup/plugin-node-resolve";
import swc from "@rollup/plugin-swc";
import copy from "rollup-plugin-copy";
export default {
input: "index.ts",
output: {
dir: "../dist/sfe",
format: "cjs",
},
context: "window",
plugins: [
copy({
targets: [
{ src: "node_modules/bootstrap/dist/css/bootstrap.min.css", dest: "../dist/sfe" },
],
}),
resolve({ browser: true }),
commonjs(),
swc({
swc: {
jsc: {
loose: false,
externalHelpers: false,
// Requires v1.2.50 or upper and requires target to be es2016 or upper.
keepClassNames: false,
},
minify: false,
env: {
targets: {
edge: "17",
ie: "11",
},
mode: "entry",
},
},
}),
],
};

7
web/sfe/tsconfig.json Normal file
View File

@ -0,0 +1,7 @@
{
"compilerOptions": {
"types": ["jquery"],
"esModuleInterop": true,
"lib": ["DOM", "ES2015", "ES2017"]
},
}

View File

@ -208,7 +208,14 @@ export class AdminOverviewPage extends AdminOverviewBase {
return html`<li> return html`<li>
${ex( ${ex(
() => html`<a href="${url}" class="pf-u-mb-xl" target="_blank">${content}</a>`, () =>
html`<a
href="${url}"
class="pf-u-mb-xl"
rel="noopener noreferrer"
target="_blank"
>${content}</a
>`,
() => html`<a href="${url}" class="pf-u-mb-xl" )>${content}</a>`, () => html`<a href="${url}" class="pf-u-mb-xl" )>${content}</a>`,
)} )}
</li>`; </li>`;

View File

@ -56,6 +56,6 @@ export class VersionStatusCard extends AdminStatusCard<Version> {
text = this.value.buildHash?.substring(0, 7); text = this.value.buildHash?.substring(0, 7);
link = `https://github.com/goauthentik/authentik/commit/${this.value.buildHash}`; link = `https://github.com/goauthentik/authentik/commit/${this.value.buildHash}`;
} }
return html`<a href=${link} target="_blank">${text}</a>`; return html`<a rel="noopener noreferrer" href=${link} target="_blank">${text}</a>`;
} }
} }

View File

@ -56,6 +56,7 @@ export class OutpostStatusChart extends AKChart<SummarizedSyncStatus[]> {
}), }),
); );
this.centerText = outposts.pagination.count.toString(); this.centerText = outposts.pagination.count.toString();
outpostStats.sort((a, b) => a.label.localeCompare(b.label));
return outpostStats; return outpostStats;
} }

View File

@ -15,7 +15,6 @@ const doGroupBy = (items: Provider[]) => groupBy(items, (item) => item.verboseNa
async function fetch(query?: string) { async function fetch(query?: string) {
const args: ProvidersAllListRequest = { const args: ProvidersAllListRequest = {
ordering: "name", ordering: "name",
backchannel: false,
}; };
if (query !== undefined) { if (query !== undefined) {
args.search = query; args.search = query;

View File

@ -157,6 +157,7 @@ export class BlueprintForm extends ModelForm<BlueprintInstance, string> {
${msg("See more about OCI support here:")}&nbsp; ${msg("See more about OCI support here:")}&nbsp;
<a <a
target="_blank" target="_blank"
rel="noopener noreferrer"
href="${docLink( href="${docLink(
"/developer-docs/blueprints/?utm_source=authentik#storage---oci", "/developer-docs/blueprints/?utm_source=authentik#storage---oci",
)}" )}"

View File

@ -23,6 +23,7 @@ export class OutpostDeploymentModal extends ModalButton {
<a <a
target="_blank" target="_blank"
href="${docLink("/docs/outposts?utm_source=authentik#deploy")}" href="${docLink("/docs/outposts?utm_source=authentik#deploy")}"
rel="noopener noreferrer"
>${msg("View deployment documentation")}</a >${msg("View deployment documentation")}</a
> >
</p> </p>

View File

@ -210,9 +210,11 @@ export class OutpostForm extends ModelForm<Outpost, string> {
)} )}
</p> </p>
<p class="pf-c-form__helper-text"> <p class="pf-c-form__helper-text">
See <a
<a target="_blank" href="${docLink("/docs/outposts?utm_source=authentik")}" target="_blank"
>documentation</a rel="noopener noreferrer"
href="${docLink("/docs/outposts?utm_source=authentik")}"
>${msg("See documentation")}</a
>. >.
</p> </p>
</ak-form-element-horizontal> </ak-form-element-horizontal>
@ -245,6 +247,7 @@ export class OutpostForm extends ModelForm<Outpost, string> {
${msg("See more here:")}&nbsp; ${msg("See more here:")}&nbsp;
<a <a
target="_blank" target="_blank"
rel="noopener noreferrer"
href="${docLink( href="${docLink(
"/docs/outposts?utm_source=authentik#configuration", "/docs/outposts?utm_source=authentik#configuration",
)}" )}"

View File

@ -85,6 +85,7 @@ export class ExpressionPolicyForm extends BasePolicyForm<ExpressionPolicy> {
<p class="pf-c-form__helper-text"> <p class="pf-c-form__helper-text">
${msg("Expression using Python.")} ${msg("Expression using Python.")}
<a <a
rel="noopener noreferrer"
target="_blank" target="_blank"
href="${docLink("/docs/policies/expression?utm_source=authentik")}" href="${docLink("/docs/policies/expression?utm_source=authentik")}"
> >

View File

@ -62,6 +62,7 @@ export class PropertyMappingGoogleWorkspaceForm extends BasePropertyMappingForm<
${msg("Expression using Python.")} ${msg("Expression using Python.")}
<a <a
target="_blank" target="_blank"
rel="noopener noreferrer"
href="${docLink("/docs/property-mappings/expression?utm_source=authentik")}" href="${docLink("/docs/property-mappings/expression?utm_source=authentik")}"
> >
${msg("See documentation for a list of all variables.")} ${msg("See documentation for a list of all variables.")}

View File

@ -71,6 +71,7 @@ export class PropertyMappingLDAPForm extends BasePropertyMappingForm<LDAPPropert
${msg("Expression using Python.")} ${msg("Expression using Python.")}
<a <a
target="_blank" target="_blank"
rel="noopener noreferrer"
href="${docLink("/docs/property-mappings/expression?utm_source=authentik")}" href="${docLink("/docs/property-mappings/expression?utm_source=authentik")}"
> >
${msg("See documentation for a list of all variables.")} ${msg("See documentation for a list of all variables.")}

View File

@ -62,6 +62,7 @@ export class PropertyMappingMicrosoftEntraForm extends BasePropertyMappingForm<M
${msg("Expression using Python.")} ${msg("Expression using Python.")}
<a <a
target="_blank" target="_blank"
rel="noopener noreferrer"
href="${docLink("/docs/property-mappings/expression?utm_source=authentik")}" href="${docLink("/docs/property-mappings/expression?utm_source=authentik")}"
> >
${msg("See documentation for a list of all variables.")} ${msg("See documentation for a list of all variables.")}

View File

@ -62,6 +62,7 @@ export class PropertyMappingNotification extends ModelForm<NotificationWebhookMa
${msg("Expression using Python.")} ${msg("Expression using Python.")}
<a <a
target="_blank" target="_blank"
rel="noopener noreferrer"
href="${docLink("/docs/property-mappings/expression?utm_source=authentik")}" href="${docLink("/docs/property-mappings/expression?utm_source=authentik")}"
> >
${msg("See documentation for a list of all variables.")} ${msg("See documentation for a list of all variables.")}

View File

@ -160,6 +160,7 @@ export class PropertyMappingLDAPForm extends ModelForm<RACPropertyMapping, strin
${msg("Expression using Python.")} ${msg("Expression using Python.")}
<a <a
target="_blank" target="_blank"
rel="noopener noreferrer"
href="${docLink( href="${docLink(
"/docs/property-mappings/expression?utm_source=authentik", "/docs/property-mappings/expression?utm_source=authentik",
)}" )}"

View File

@ -83,6 +83,7 @@ export class PropertyMappingSAMLForm extends BasePropertyMappingForm<SAMLPropert
${msg("Expression using Python.")} ${msg("Expression using Python.")}
<a <a
target="_blank" target="_blank"
rel="noopener noreferrer"
href="${docLink("/docs/property-mappings/expression?utm_source=authentik")}" href="${docLink("/docs/property-mappings/expression?utm_source=authentik")}"
> >
${msg("See documentation for a list of all variables.")} ${msg("See documentation for a list of all variables.")}

View File

@ -56,6 +56,7 @@ export class PropertyMappingSCIMForm extends BasePropertyMappingForm<SCIMMapping
${msg("Expression using Python.")} ${msg("Expression using Python.")}
<a <a
target="_blank" target="_blank"
rel="noopener noreferrer"
href="${docLink("/docs/property-mappings/expression?utm_source=authentik")}" href="${docLink("/docs/property-mappings/expression?utm_source=authentik")}"
> >
${msg("See documentation for a list of all variables.")} ${msg("See documentation for a list of all variables.")}

View File

@ -83,6 +83,7 @@ export class PropertyMappingScopeForm extends BasePropertyMappingForm<ScopeMappi
${msg("Expression using Python.")} ${msg("Expression using Python.")}
<a <a
target="_blank" target="_blank"
rel="noopener noreferrer"
href="${docLink("/docs/property-mappings/expression?utm_source=authentik")}" href="${docLink("/docs/property-mappings/expression?utm_source=authentik")}"
> >
${msg("See documentation for a list of all variables.")} ${msg("See documentation for a list of all variables.")}

View File

@ -1,3 +1,7 @@
import {
digestAlgorithmOptions,
signatureAlgorithmOptions,
} from "@goauthentik/admin/applications/wizard/methods/saml/SamlProviderOptions";
import "@goauthentik/admin/common/ak-crypto-certificate-search"; import "@goauthentik/admin/common/ak-crypto-certificate-search";
import "@goauthentik/admin/common/ak-flow-search/ak-flow-search"; import "@goauthentik/admin/common/ak-flow-search/ak-flow-search";
import { BaseProviderForm } from "@goauthentik/admin/providers/BaseProviderForm"; import { BaseProviderForm } from "@goauthentik/admin/providers/BaseProviderForm";
@ -14,7 +18,6 @@ import { customElement } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js"; import { ifDefined } from "lit/directives/if-defined.js";
import { import {
DigestAlgorithmEnum,
FlowsInstancesListDesignationEnum, FlowsInstancesListDesignationEnum,
PaginatedSAMLPropertyMappingList, PaginatedSAMLPropertyMappingList,
PropertymappingsApi, PropertymappingsApi,
@ -22,7 +25,6 @@ import {
ProvidersApi, ProvidersApi,
SAMLPropertyMapping, SAMLPropertyMapping,
SAMLProvider, SAMLProvider,
SignatureAlgorithmEnum,
SpBindingEnum, SpBindingEnum,
} from "@goauthentik/api"; } from "@goauthentik/api";
@ -333,25 +335,7 @@ export class SAMLProviderFormPage extends BaseProviderForm<SAMLProvider> {
name="digestAlgorithm" name="digestAlgorithm"
> >
<ak-radio <ak-radio
.options=${[ .options=${digestAlgorithmOptions}
{
label: "SHA1",
value: DigestAlgorithmEnum._200009Xmldsigsha1,
},
{
label: "SHA256",
value: DigestAlgorithmEnum._200104Xmlencsha256,
default: true,
},
{
label: "SHA384",
value: DigestAlgorithmEnum._200104XmldsigMoresha384,
},
{
label: "SHA512",
value: DigestAlgorithmEnum._200104Xmlencsha512,
},
]}
.value=${this.instance?.digestAlgorithm} .value=${this.instance?.digestAlgorithm}
> >
</ak-radio> </ak-radio>
@ -362,29 +346,7 @@ export class SAMLProviderFormPage extends BaseProviderForm<SAMLProvider> {
name="signatureAlgorithm" name="signatureAlgorithm"
> >
<ak-radio <ak-radio
.options=${[ .options=${signatureAlgorithmOptions}
{
label: "RSA-SHA1",
value: SignatureAlgorithmEnum._200009XmldsigrsaSha1,
},
{
label: "RSA-SHA256",
value: SignatureAlgorithmEnum._200104XmldsigMorersaSha256,
default: true,
},
{
label: "RSA-SHA384",
value: SignatureAlgorithmEnum._200104XmldsigMorersaSha384,
},
{
label: "RSA-SHA512",
value: SignatureAlgorithmEnum._200104XmldsigMorersaSha512,
},
{
label: "DSA-SHA1",
value: SignatureAlgorithmEnum._200009XmldsigdsaSha1,
},
]}
.value=${this.instance?.signatureAlgorithm} .value=${this.instance?.signatureAlgorithm}
> >
</ak-radio> </ak-radio>

View File

@ -36,11 +36,13 @@ import "@goauthentik/elements/oauth/UserRefreshTokenList";
import "@goauthentik/elements/rbac/ObjectPermissionsPage"; import "@goauthentik/elements/rbac/ObjectPermissionsPage";
import "@goauthentik/elements/user/SessionList"; import "@goauthentik/elements/user/SessionList";
import "@goauthentik/elements/user/UserConsentList"; import "@goauthentik/elements/user/UserConsentList";
import "@goauthentik/elements/user/UserReputationList";
import "@goauthentik/elements/user/sources/SourceSettings"; import "@goauthentik/elements/user/sources/SourceSettings";
import { msg, str } from "@lit/localize"; import { msg, str } from "@lit/localize";
import { TemplateResult, css, html, nothing } from "lit"; import { TemplateResult, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators.js"; import { customElement, property, state } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import PFBanner from "@patternfly/patternfly/components/Banner/banner.css"; import PFBanner from "@patternfly/patternfly/components/Banner/banner.css";
import PFButton from "@patternfly/patternfly/components/Button/button.css"; import PFButton from "@patternfly/patternfly/components/Button/button.css";
@ -274,6 +276,21 @@ export class UserViewPage extends WithCapabilitiesConfig(AKElement) {
</div> </div>
</div> </div>
</section> </section>
<section
slot="page-reputation"
data-tab-title="${msg("Reputation scores")}"
class="pf-c-page__main-section pf-m-no-padding-mobile"
>
<div class="pf-c-card">
<div class="pf-c-card__body">
<ak-user-reputation-list
targetUsername=${user.username}
targetEmail=${ifDefined(user.email)}
>
</ak-user-reputation-list>
</div>
</div>
</section>
<section <section
slot="page-consent" slot="page-consent"
data-tab-title="${msg("Explicit Consent")}" data-tab-title="${msg("Explicit Consent")}"

View File

@ -3,7 +3,7 @@ export const SUCCESS_CLASS = "pf-m-success";
export const ERROR_CLASS = "pf-m-danger"; export const ERROR_CLASS = "pf-m-danger";
export const PROGRESS_CLASS = "pf-m-in-progress"; export const PROGRESS_CLASS = "pf-m-in-progress";
export const CURRENT_CLASS = "pf-m-current"; export const CURRENT_CLASS = "pf-m-current";
export const VERSION = "2024.6.0"; export const VERSION = "2024.6.3";
export const TITLE_DEFAULT = "authentik"; export const TITLE_DEFAULT = "authentik";
export const ROUTE_SEPARATOR = ";"; export const ROUTE_SEPARATOR = ";";

View File

@ -1,4 +1,5 @@
import { EVENT_THEME_CHANGE } from "@goauthentik/common/constants"; import { EVENT_THEME_CHANGE } from "@goauthentik/common/constants";
import { globalAK } from "@goauthentik/common/global";
import { UIConfig } from "@goauthentik/common/ui/config"; import { UIConfig } from "@goauthentik/common/ui/config";
import { adaptCSS } from "@goauthentik/common/utils"; import { adaptCSS } from "@goauthentik/common/utils";
import { ensureCSSStyleSheet } from "@goauthentik/elements/utils/ensureCSSStyleSheet"; import { ensureCSSStyleSheet } from "@goauthentik/elements/utils/ensureCSSStyleSheet";
@ -16,6 +17,7 @@ type AkInterface = HTMLElement & {
brand?: CurrentBrand; brand?: CurrentBrand;
uiConfig?: UIConfig; uiConfig?: UIConfig;
config?: Config; config?: Config;
get activeTheme(): UiThemeEnum | undefined;
}; };
export const rootInterface = <T extends AkInterface>(): T | undefined => export const rootInterface = <T extends AkInterface>(): T | undefined =>
@ -41,7 +43,11 @@ function fetchCustomCSS(): Promise<string[]> {
return css; return css;
} }
const QUERY_MEDIA_COLOR_LIGHT = "(prefers-color-scheme: light)"; export const QUERY_MEDIA_COLOR_LIGHT = "(prefers-color-scheme: light)";
// Ensure themes are converted to a static instance of CSS Stylesheet, otherwise the
// when changing themes we might not remove the correct css stylesheet instance.
const _darkTheme = ensureCSSStyleSheet(ThemeDark);
@localized() @localized()
export class AKElement extends LitElement { export class AKElement extends LitElement {
@ -90,12 +96,7 @@ export class AKElement extends LitElement {
async _initTheme(root: DocumentOrShadowRoot): Promise<void> { async _initTheme(root: DocumentOrShadowRoot): Promise<void> {
// Early activate theme based on media query to prevent light flash // Early activate theme based on media query to prevent light flash
// when dark is preferred // when dark is preferred
this._activateTheme( this._applyTheme(root, globalAK().brand.uiTheme);
root,
window.matchMedia(QUERY_MEDIA_COLOR_LIGHT).matches
? UiThemeEnum.Light
: UiThemeEnum.Dark,
);
this._applyTheme(root, await this.getTheme()); this._applyTheme(root, await this.getTheme());
} }
@ -125,8 +126,9 @@ export class AKElement extends LitElement {
ev?.matches || this._mediaMatcher?.matches ev?.matches || this._mediaMatcher?.matches
? UiThemeEnum.Light ? UiThemeEnum.Light
: UiThemeEnum.Dark; : UiThemeEnum.Dark;
this._activateTheme(root, theme); this._activateTheme(theme, root);
}; };
this._mediaMatcherHandler(undefined);
this._mediaMatcher.addEventListener("change", this._mediaMatcherHandler); this._mediaMatcher.addEventListener("change", this._mediaMatcherHandler);
} }
return; return;
@ -136,17 +138,21 @@ export class AKElement extends LitElement {
this._mediaMatcher.removeEventListener("change", this._mediaMatcherHandler); this._mediaMatcher.removeEventListener("change", this._mediaMatcherHandler);
this._mediaMatcher = undefined; this._mediaMatcher = undefined;
} }
this._activateTheme(root, theme); this._activateTheme(theme, root);
} }
static themeToStylesheet(theme?: UiThemeEnum): CSSStyleSheet | undefined { static themeToStylesheet(theme?: UiThemeEnum): CSSStyleSheet | undefined {
if (theme === UiThemeEnum.Dark) { if (theme === UiThemeEnum.Dark) {
return ThemeDark; return _darkTheme;
} }
return undefined; return undefined;
} }
_activateTheme(root: DocumentOrShadowRoot, theme: UiThemeEnum) { /**
* Directly activate a given theme, accepts multiple document/ShadowDOMs to apply the stylesheet
* to. The stylesheets are applied to each DOM in order. Does nothing if the given theme is already active.
*/
_activateTheme(theme: UiThemeEnum, ...roots: DocumentOrShadowRoot[]) {
if (theme === this._activeTheme) { if (theme === this._activeTheme) {
return; return;
} }
@ -161,12 +167,19 @@ export class AKElement extends LitElement {
this.setAttribute("theme", theme); this.setAttribute("theme", theme);
const stylesheet = AKElement.themeToStylesheet(theme); const stylesheet = AKElement.themeToStylesheet(theme);
const oldStylesheet = AKElement.themeToStylesheet(this._activeTheme); const oldStylesheet = AKElement.themeToStylesheet(this._activeTheme);
if (stylesheet) { roots.forEach((root) => {
root.adoptedStyleSheets = [...root.adoptedStyleSheets, ensureCSSStyleSheet(stylesheet)]; if (stylesheet) {
} root.adoptedStyleSheets = [
if (oldStylesheet) { ...root.adoptedStyleSheets,
root.adoptedStyleSheets = root.adoptedStyleSheets.filter((v) => v !== oldStylesheet); ensureCSSStyleSheet(stylesheet),
} ];
}
if (oldStylesheet) {
root.adoptedStyleSheets = root.adoptedStyleSheets.filter(
(v) => v !== oldStylesheet,
);
}
});
this._activeTheme = theme; this._activeTheme = theme;
this.requestUpdate(); this.requestUpdate();
} }

View File

@ -9,7 +9,7 @@ import PFBase from "@patternfly/patternfly/patternfly-base.css";
import type { Config, CurrentBrand, LicenseSummary } from "@goauthentik/api"; import type { Config, CurrentBrand, LicenseSummary } from "@goauthentik/api";
import { UiThemeEnum } from "@goauthentik/api"; import { UiThemeEnum } from "@goauthentik/api";
import { AKElement } from "../Base"; import { AKElement, rootInterface } from "../Base";
import { BrandContextController } from "./BrandContextController"; import { BrandContextController } from "./BrandContextController";
import { ConfigContextController } from "./ConfigContextController"; import { ConfigContextController } from "./ConfigContextController";
import { EnterpriseContextController } from "./EnterpriseContextController"; import { EnterpriseContextController } from "./EnterpriseContextController";
@ -50,9 +50,19 @@ export class Interface extends AKElement implements AkInterface {
this.dataset.akInterfaceRoot = "true"; this.dataset.akInterfaceRoot = "true";
} }
_activateTheme(root: DocumentOrShadowRoot, theme: UiThemeEnum): void { _activateTheme(theme: UiThemeEnum, ...roots: DocumentOrShadowRoot[]): void {
super._activateTheme(root, theme); if (theme === this._activeTheme) {
super._activateTheme(document as unknown as DocumentOrShadowRoot, theme); return;
}
console.debug(
`authentik/interface[${rootInterface()?.tagName.toLowerCase()}]: Enabling theme ${theme}`,
);
// Special case for root interfaces, as they need to modify the global document CSS too
// Instead of calling ._activateTheme() twice, we insert the root document in the call
// since multiple calls to ._activateTheme() would not do anything after the first call
// as the theme is already enabled.
roots.unshift(document as unknown as DocumentOrShadowRoot);
super._activateTheme(theme, ...roots);
} }
async getTheme(): Promise<UiThemeEnum> { async getTheme(): Promise<UiThemeEnum> {

View File

@ -78,7 +78,7 @@ export class Markdown extends AKElement {
const pathName = path.replace(".md", ""); const pathName = path.replace(".md", "");
const link = `docs/${baseName}${pathName}`; const link = `docs/${baseName}${pathName}`;
const url = new URL(link, baseUrl).toString(); const url = new URL(link, baseUrl).toString();
return `href="${url}" _target="blank"`; return `href="${url}" _target="blank" rel="noopener noreferrer"`;
}); });
} }

View File

@ -1,7 +1,8 @@
import { EVENT_LOCALE_CHANGE, EVENT_LOCALE_REQUEST } from "@goauthentik/common/constants"; import { EVENT_LOCALE_CHANGE, EVENT_LOCALE_REQUEST } from "@goauthentik/common/constants";
import { AKElement } from "@goauthentik/elements/Base";
import { customEvent } from "@goauthentik/elements/utils/customEvents"; import { customEvent } from "@goauthentik/elements/utils/customEvents";
import { LitElement, html } from "lit"; import { html } from "lit";
import { customElement, property } from "lit/decorators.js"; import { customElement, property } from "lit/decorators.js";
import { WithBrandConfig } from "../Interface/brandProvider"; import { WithBrandConfig } from "../Interface/brandProvider";
@ -9,8 +10,6 @@ import { initializeLocalization } from "./configureLocale";
import type { LocaleGetter, LocaleSetter } from "./configureLocale"; import type { LocaleGetter, LocaleSetter } from "./configureLocale";
import { DEFAULT_LOCALE, autoDetectLanguage, getBestMatchLocale } from "./helpers"; import { DEFAULT_LOCALE, autoDetectLanguage, getBestMatchLocale } from "./helpers";
const LocaleContextBase = WithBrandConfig(LitElement);
/** /**
* A component to manage your locale settings. * A component to manage your locale settings.
* *
@ -25,7 +24,7 @@ const LocaleContextBase = WithBrandConfig(LitElement);
* @fires ak-locale-change - When a valid locale has been swapped in * @fires ak-locale-change - When a valid locale has been swapped in
*/ */
@customElement("ak-locale-context") @customElement("ak-locale-context")
export class LocaleContext extends LocaleContextBase { export class LocaleContext extends WithBrandConfig(AKElement) {
/// @attribute The text representation of the current locale */ /// @attribute The text representation of the current locale */
@property({ attribute: true, type: String }) @property({ attribute: true, type: String })
locale = DEFAULT_LOCALE; locale = DEFAULT_LOCALE;
@ -78,7 +77,7 @@ export class LocaleContext extends LocaleContextBase {
return; return;
} }
locale.locale().then(() => { locale.locale().then(() => {
console.debug(`Setting Locale to ... ${locale.label()} (${locale.code})`); console.debug(`authentik/locale: Setting Locale to ${locale.label()} (${locale.code})`);
this.setLocale(locale.code).then(() => { this.setLocale(locale.code).then(() => {
window.setTimeout(this.notifyApplication, 0); window.setTimeout(this.notifyApplication, 0);
}); });

View File

@ -66,15 +66,15 @@ export class UserOAuthAccessTokenList extends Table<TokenModel> {
renderToolbarSelected(): TemplateResult { renderToolbarSelected(): TemplateResult {
const disabled = this.selectedElements.length < 1; const disabled = this.selectedElements.length < 1;
return html`<ak-forms-delete-bulk return html`<ak-forms-delete-bulk
objectLabel=${msg("Refresh Tokens(s)")} objectLabel=${msg("Access Tokens(s)")}
.objects=${this.selectedElements} .objects=${this.selectedElements}
.usedBy=${(item: ExpiringBaseGrantModel) => { .usedBy=${(item: ExpiringBaseGrantModel) => {
return new Oauth2Api(DEFAULT_CONFIG).oauth2RefreshTokensUsedByList({ return new Oauth2Api(DEFAULT_CONFIG).oauth2AccessTokensUsedByList({
id: item.pk, id: item.pk,
}); });
}} }}
.delete=${(item: ExpiringBaseGrantModel) => { .delete=${(item: ExpiringBaseGrantModel) => {
return new Oauth2Api(DEFAULT_CONFIG).oauth2RefreshTokensDestroy({ return new Oauth2Api(DEFAULT_CONFIG).oauth2AccessTokensDestroy({
id: item.pk, id: item.pk,
}); });
}} }}

View File

@ -1,6 +1,7 @@
import { EVENT_SIDEBAR_TOGGLE } from "@goauthentik/common/constants"; import { EVENT_SIDEBAR_TOGGLE } from "@goauthentik/common/constants";
import { AKElement } from "@goauthentik/elements/Base"; import { AKElement } from "@goauthentik/elements/Base";
import { WithBrandConfig } from "@goauthentik/elements/Interface/brandProvider"; import { WithBrandConfig } from "@goauthentik/elements/Interface/brandProvider";
import { themeImage } from "@goauthentik/elements/utils/images";
import { CSSResult, TemplateResult, css, html } from "lit"; import { CSSResult, TemplateResult, css, html } from "lit";
import { customElement } from "lit/decorators.js"; import { customElement } from "lit/decorators.js";
@ -84,7 +85,7 @@ export class SidebarBrand extends WithBrandConfig(AKElement) {
<a href="#/" class="pf-c-page__header-brand-link"> <a href="#/" class="pf-c-page__header-brand-link">
<div class="pf-c-brand ak-brand"> <div class="pf-c-brand ak-brand">
<img <img
src=${this.brand?.brandingLogo ?? DefaultBrand.brandingLogo} src=${themeImage(this.brand?.brandingLogo ?? DefaultBrand.brandingLogo)}
alt="authentik Logo" alt="authentik Logo"
loading="lazy" loading="lazy"
/> />

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