Compare commits

...

82 Commits

Author SHA1 Message Date
2a09fc0ae2 release: 2021.12.1-rc5 2021-12-15 10:21:29 +01:00
fbb6756488 Merge branch 'master' into version-2021.12 2021-12-15 10:16:05 +01:00
f45fb2eac0 website/docs: prepare 2021.12.1-rc5
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-15 10:15:58 +01:00
7b8cde17e6 web/admin: show warning when deleting currently logged in user
closes #1937

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-15 10:11:35 +01:00
186634fc67 build(deps): bump @patternfly/patternfly from 4.159.1 to 4.164.2 in /web (#1938) 2021-12-15 08:58:46 +01:00
c84b1b7997 build(deps): bump goauthentik.io/api from 0.2021104.13 to 0.2021104.17 (#1939) 2021-12-15 08:58:30 +01:00
6e83467481 web/flows: fix error when attempting to enroll new webauthn device
closes #1936

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-15 00:24:46 +01:00
72db17f23b stages/identification: fix miscalculated sleep
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-14 23:31:08 +01:00
ee4e176039 web/admin: fix invalid display for LDAP Source sync status
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-14 23:00:45 +01:00
e18e681c2b events: dont store full backtrace in systemtask
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-14 22:55:38 +01:00
10fe67e08d sources/ldap: fix incorrect task names being referenced, use source native slug
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-14 22:53:14 +01:00
fc1db83be7 web: Update Web API Client version (#1935)
Signed-off-by: GitHub <noreply@github.com>

Co-authored-by: BeryJu <BeryJu@users.noreply.github.com>
2021-12-14 22:19:46 +01:00
3740e65906 web/admin: add dashboard with user creation/login statistics
closes #1867

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-14 22:08:41 +01:00
30386cd899 events: add custom manager with helpers for metrics
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-14 21:49:33 +01:00
64a10e9a46 events: fix schema for top_per_user
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-14 21:08:15 +01:00
77d6242cce web/admin: fix extra closing element
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-14 20:53:25 +01:00
9a86dcaec3 web: Update Web API Client version (#1934)
Signed-off-by: GitHub <noreply@github.com>

Co-authored-by: BeryJu <BeryJu@users.noreply.github.com>
2021-12-14 16:26:21 +01:00
0b00768b84 events: add flow_execution event type
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-14 16:13:51 +01:00
d162c79373 flows: fix wrong exception being caught in flow inspector
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-14 16:06:00 +01:00
05db352a0f web: add link to open API Browser for API Drawer
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-14 16:03:42 +01:00
5bf3d7fe02 web: Update Web API Client version (#1933)
Signed-off-by: GitHub <noreply@github.com>

Co-authored-by: BeryJu <BeryJu@users.noreply.github.com>
2021-12-14 15:59:26 +01:00
1ae1cbebf4 web/admin: re-organise sidebar items
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-14 15:57:03 +01:00
8c16dfc478 stages/invitation: use GroupMemberSerializer serializer to prevent all of the user's groups and their users from being returned
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-14 15:56:13 +01:00
c6a3286e4c web/admin: update overview page
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-14 15:23:32 +01:00
44cfd7e5b0 web: accept header as slot in PageHeader
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-14 15:23:20 +01:00
210d4c5058 web: add helper to navigate with params
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-14 15:23:02 +01:00
6b39d616b1 web/elements: allow aggregate cards' elements to not be centered
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-14 15:22:52 +01:00
32ace1bece crypto: add additional validation before importing a certificate
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-14 14:49:25 +01:00
54f893b84f flows: add additional sentry spans
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-14 11:59:36 +01:00
b5685ec072 outposts: set sentry-trace on API requests to match them to the outer transaction
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-14 11:50:31 +01:00
5854833240 stages/authenticator_webauthn: fix migrations for different choices
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-14 11:06:46 +01:00
4b2437a6f1 stages/authenticator_webauthn: use correct choices
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-14 10:51:34 +01:00
2981ac7b10 tests/e2e: use ghcr for e2e tests
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-14 10:36:47 +01:00
59a51c859a stages/authenticator_webauthn: add migration
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-14 10:09:35 +01:00
47bab6c182 web: Update Web API Client version (#1932)
Signed-off-by: GitHub <noreply@github.com>

Co-authored-by: BeryJu <BeryJu@users.noreply.github.com>
2021-12-14 10:02:50 +01:00
4e6714fffe stages/authenticator_webauthn: make user_verification configurable
closes #1921

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-14 09:58:20 +01:00
aa6b595545 root: bump python dependencies
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-14 09:45:36 +01:00
0131b1f6cc sources/oauth: fix wrong redirect URL being generated
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-14 09:34:47 +01:00
9f53c359dd build(deps): bump typescript from 4.5.3 to 4.5.4 in /web (#1926) 2021-12-14 08:31:56 +01:00
28e4dba3e8 build(deps): bump @babel/core from 7.16.0 to 7.16.5 in /web (#1929) 2021-12-14 08:31:36 +01:00
2afd46e1df build(deps): bump @babel/plugin-transform-runtime in /web (#1922) 2021-12-14 08:31:28 +01:00
f5991b19be build(deps): bump @babel/preset-typescript from 7.16.0 to 7.16.5 in /web (#1925) 2021-12-14 08:31:19 +01:00
5cc75cb25c build(deps): bump @typescript-eslint/parser from 5.6.0 to 5.7.0 in /web (#1923) 2021-12-14 08:31:04 +01:00
68c1df2d39 build(deps): bump @rollup/plugin-node-resolve in /web (#1924) 2021-12-14 08:30:35 +01:00
c83724f45c build(deps): bump @babel/preset-env from 7.16.4 to 7.16.5 in /web (#1930) 2021-12-14 08:29:56 +01:00
5f91c150df build(deps): bump @babel/plugin-proposal-decorators in /web (#1927) 2021-12-14 08:29:36 +01:00
0bfe999442 build(deps): bump @typescript-eslint/eslint-plugin in /web (#1928) 2021-12-14 08:29:13 +01:00
58440b16c4 build(deps): bump goauthentik.io/api from 0.2021104.11 to 0.2021104.13 (#1931) 2021-12-14 08:28:42 +01:00
57757a2ff5 web: Update Web API Client version (#1920) 2021-12-14 01:11:32 +01:00
2993f506a7 sources/oauth: implement apple native sign-in using the apple JS SDK
closes #1881

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-14 00:40:29 +01:00
e4841d54a1 *: migrate ui_* properties to functions to allow context being passed
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-13 23:56:35 +01:00
4f05dcec89 sources/oauth: allow oauth types to override their login button challenge
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-13 23:45:11 +01:00
ede6bcd31e *: remove debug statements from tests
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-13 23:41:08 +01:00
728c8e994d sources/oauth: strip parts of custom apple client_id
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-13 23:26:00 +01:00
5290b64415 web: Update Web API Client version
commit 96533a743c
Author: BeryJu <BeryJu@users.noreply.github.com>
Date:   Mon Dec 13 20:50:45 2021 +0000

    web: Update Web API Client version

    Signed-off-by: GitHub <noreply@github.com>

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-13 22:53:28 +01:00
fec6de1ba2 providers/oauth2: add additional logging to show with token path is taken
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-13 22:49:42 +01:00
69678dcfa6 providers/oauth2: use generate_key instead of uuid4
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-13 22:13:20 +01:00
4911a243ff sources/oauth: add initial okta type
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

#1910
2021-12-13 21:48:59 +01:00
70316b37da web/admin: only show source name not description
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-13 21:48:45 +01:00
307cb94e3b website: add initial redirect (#1918)
* website: add initial redirect

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

* website: add integrations too

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

* website: add docs to netlify config

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

* website: use splats correctly

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

* add status

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-13 20:42:31 +00:00
ace53a8fa5 root: remove lxml version workaround
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-13 21:08:50 +01:00
0544dc3f83 web: use correct transaction names for web
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-13 21:03:30 +01:00
708ff300a3 website: remove remaining /index URLs
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-13 19:01:16 +01:00
4e63f0f215 core: add fallback for missing sentry trace
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-13 18:06:01 +01:00
141481df3a web: send sentry-trace header in API requests
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-13 17:41:11 +01:00
29241cc287 core: always inject sentry trace into template
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-13 17:41:00 +01:00
e81e97d404 root: add .python-version so dependabot doesn't use broken python 3.10.0
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-13 17:23:42 +01:00
a5182e5c24 root: custom sentry-sdk, attempt #3
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-13 17:00:18 +01:00
cf5ff6e160 outposts: reset backoff after successful connect
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-13 16:38:48 +01:00
f2b3a2ec91 providers/saml: optimise excessive queries to user when evaluating attributes
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-13 16:38:38 +01:00
69780c67a9 lib: set evaluation span's description based on filename
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-13 16:32:01 +01:00
ac9cf590bc *: use prefixed span names
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-13 16:18:42 +01:00
cb6edcb198 core: set tag with request ID
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-13 16:15:27 +01:00
8eecc28c3c events: add sentry for geoip
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-13 16:15:20 +01:00
10b16bc36a outposts: add description to span
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-13 16:12:14 +01:00
2fe88cfea9 root: don't stale enhancement/confirmed
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-13 15:51:30 +01:00
caab396b56 web/admin: improve wording for froward_auth, don't show setup when using proxy mode
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-13 15:36:05 +01:00
5f0f4284a2 web/admin: fix rendering for applications on view page
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-13 15:27:28 +01:00
c11be2284d outposts/proxy: also set max length for redis backend
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-13 15:05:55 +01:00
aa321196d7 outposts/proxy: fix securecookie: the value is too long again, since it can happen even with filesystem storage
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-13 13:33:20 +01:00
ff03db61a8 web/admin: fix rendering of applications with custom icon
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-13 13:21:14 +01:00
f3b3ce6572 website/docs: add 2021.12.1-rc4 release notes
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-12-13 12:56:34 +01:00
133 changed files with 2911 additions and 1771 deletions

View File

@ -1,5 +1,5 @@
[bumpversion]
current_version = 2021.12.1-rc4
current_version = 2021.12.1-rc5
tag = True
commit = True
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-?(?P<release>.*)

1
.github/stale.yml vendored
View File

@ -7,6 +7,7 @@ exemptLabels:
- pinned
- security
- pr_wanted
- enhancement/confirmed
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
This issue has been automatically marked as stale because it has not had

View File

@ -30,14 +30,14 @@ jobs:
with:
push: ${{ github.event_name == 'release' }}
tags: |
beryju/authentik:2021.12.1-rc4,
beryju/authentik:2021.12.1-rc5,
beryju/authentik:latest,
ghcr.io/goauthentik/server:2021.12.1-rc4,
ghcr.io/goauthentik/server:2021.12.1-rc5,
ghcr.io/goauthentik/server:latest
platforms: linux/amd64,linux/arm64
context: .
- name: Building Docker Image (stable)
if: ${{ github.event_name == 'release' && !contains('2021.12.1-rc4', 'rc') }}
if: ${{ github.event_name == 'release' && !contains('2021.12.1-rc5', 'rc') }}
run: |
docker pull beryju/authentik:latest
docker tag beryju/authentik:latest beryju/authentik:stable
@ -78,14 +78,14 @@ jobs:
with:
push: ${{ github.event_name == 'release' }}
tags: |
beryju/authentik-${{ matrix.type }}:2021.12.1-rc4,
beryju/authentik-${{ matrix.type }}:2021.12.1-rc5,
beryju/authentik-${{ matrix.type }}:latest,
ghcr.io/goauthentik/${{ matrix.type }}:2021.12.1-rc4,
ghcr.io/goauthentik/${{ matrix.type }}:2021.12.1-rc5,
ghcr.io/goauthentik/${{ matrix.type }}:latest
file: ${{ matrix.type }}.Dockerfile
platforms: linux/amd64,linux/arm64
- name: Building Docker Image (stable)
if: ${{ github.event_name == 'release' && !contains('2021.12.1-rc4', 'rc') }}
if: ${{ github.event_name == 'release' && !contains('2021.12.1-rc5', 'rc') }}
run: |
docker pull beryju/authentik-${{ matrix.type }}:latest
docker tag beryju/authentik-${{ matrix.type }}:latest beryju/authentik-${{ matrix.type }}:stable
@ -128,7 +128,7 @@ jobs:
SENTRY_PROJECT: authentik
SENTRY_URL: https://sentry.beryju.org
with:
version: authentik@2021.12.1-rc4
version: authentik@2021.12.1-rc5
environment: beryjuorg-prod
sourcemaps: './web/dist'
url_prefix: '~/static/dist'

1
.python-version Normal file
View File

@ -0,0 +1 @@
3.9.7

View File

@ -58,8 +58,6 @@ RUN apt-get update && \
curl ca-certificates gnupg git runit libpq-dev \
postgresql-client build-essential libxmlsec1-dev \
pkg-config libmaxminddb0 && \
pip install lxml==4.6.4 --no-cache-dir && \
export C_INCLUDE_PATH=/usr/local/lib/python3.10/site-packages/lxml/includes && \
pip install -r /requirements.txt --no-cache-dir && \
apt-get remove --purge -y build-essential git && \
apt-get autoremove --purge -y && \

View File

@ -4,7 +4,7 @@ UID = $(shell id -u)
GID = $(shell id -g)
NPM_VERSION = $(shell python -m scripts.npm_version)
all: lint-fix lint test gen
all: lint-fix lint test gen web
test-integration:
coverage run manage.py test tests/integration

View File

@ -32,15 +32,14 @@ geoip2 = "*"
gunicorn = "*"
kubernetes = "==v19.15.0"
ldap3 = "*"
# 4.7.0 and later remove `lxml-version.h` which is required by xmlsec
lxml = "==4.6.5"
lxml = "*"
packaging = "*"
psycopg2-binary = "*"
pycryptodome = "*"
pyjwt = "*"
pyyaml = "*"
requests-oauthlib = "*"
sentry-sdk = "*"
sentry-sdk = { git = 'https://github.com/beryju/sentry-python.git', ref = '379aee28b15d3b87b381317746c4efd24b3d7bc3' }
service_identity = "*"
structlog = "*"
swagger-spec-validator = "*"

156
Pipfile.lock generated
View File

@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "6a89870496296af32dbc2f64b0832d4c20010829ada0b3c4dc27fee56b68fad9"
"sha256": "dedb51159ef09fd9b00ab28022706f525c9df057ffd646e2a552784341a10538"
},
"pipfile-spec": 6,
"requires": {},
@ -169,19 +169,19 @@
},
"boto3": {
"hashes": [
"sha256:76b3ee0d1dd860c9218bc864cd29f1ee986f6e1e75e8669725dd3c411039379e",
"sha256:c39cb6ed376ba1d4689ac8f6759a2b2d8a0b0424dbec0cd3af1558079bcf06e8"
"sha256:739705b28e6b2329ea3b481ba801d439c296aaf176f7850729147ba99bbf8a9a",
"sha256:8f08e8e94bf107c5e9866684e9aadf8d9f60abed0cfe5c1dba4e7328674a1986"
],
"index": "pypi",
"version": "==1.20.23"
"version": "==1.20.24"
},
"botocore": {
"hashes": [
"sha256:640b62110aa6d1c25553eceafb5bcd89aedeb84b191598d1f6492ad24374d285",
"sha256:7459766c4594f3b8877e8013f93f0dc6c6486acbeb7d9c9ae488396529cc2e84"
"sha256:43006b4f52d7bb655319d3da0f615cdbee7762853acc1ebcb1d49f962e6b4806",
"sha256:e78d48c50c8c013fb9b362c6202fece2fe868edfd89b51968080180bdff41617"
],
"markers": "python_version >= '3.6'",
"version": "==1.23.23"
"version": "==1.23.24"
},
"cachetools": {
"hashes": [
@ -196,7 +196,7 @@
"sha256:1ef33f089e0a494e8d1b487508356f055c865b1955b125c00c991a4358543c80",
"sha256:8eca49962b1bfc09c24d442aa55688be88efe5c24aeef89d3be135614b95c678"
],
"markers": "python_version >= '3.7' and python_version < '4'",
"markers": "python_version >= '3.7' and python_full_version < '4.0.0'",
"version": "==1.9.0"
},
"cbor2": {
@ -325,7 +325,7 @@
"sha256:a0713dc7a1de3f06bc0df5a9567ad19ead2d3d5689b434768a6145bff77c0667",
"sha256:f184f0d851d96b6d29297354ed981b7dd71df7ff500d82fa6d11f0856bee8035"
],
"markers": "python_version < '4' and python_full_version >= '3.6.2'",
"markers": "python_full_version >= '3.6.2' and python_full_version < '4.0.0'",
"version": "==0.3.0"
},
"click-plugins": {
@ -494,11 +494,11 @@
},
"djangorestframework": {
"hashes": [
"sha256:6d1d59f623a5ad0509fe0d6bfe93cbdfe17b8116ebc8eda86d45f6e16e819aaf",
"sha256:f747949a8ddac876e879190df194b925c177cdeb725a099db1460872f7c0a7f2"
"sha256:48e64f08244fa0df9e2b8fbd405edec263d8e1251112a06d0073b546b7c86b9c",
"sha256:8b987d5683f5b3553dd946d4972048d3117fc526cb0bc01a3f021e81af53f39e"
],
"index": "pypi",
"version": "==3.12.4"
"version": "==3.13.0"
},
"djangorestframework-guardian": {
"hashes": [
@ -816,69 +816,69 @@
},
"lxml": {
"hashes": [
"sha256:11ae552a78612620afd15625be9f1b82e3cc2e634f90d6b11709b10a100cba59",
"sha256:121fc6f71c692b49af6c963b84ab7084402624ffbe605287da362f8af0668ea3",
"sha256:124f09614f999551ac65e5b9875981ce4b66ac4b8e2ba9284572f741935df3d9",
"sha256:12ae2339d32a2b15010972e1e2467345b7bf962e155671239fba74c229564b7f",
"sha256:12d8d6fe3ddef629ac1349fa89a638b296a34b6529573f5055d1cb4e5245f73b",
"sha256:1a2a7659b8eb93c6daee350a0d844994d49245a0f6c05c747f619386fb90ba04",
"sha256:1ccbfe5d17835db906f2bab6f15b34194db1a5b07929cba3cf45a96dbfbfefc0",
"sha256:2f77556266a8fe5428b8759fbfc4bd70be1d1d9c9b25d2a414f6a0c0b0f09120",
"sha256:3534d7c468c044f6aef3c0aff541db2826986a29ea73f2ca831f5d5284d9b570",
"sha256:3884476a90d415be79adfa4e0e393048630d0d5bcd5757c4c07d8b4b00a1096b",
"sha256:3b95fb7e6f9c2f53db88f4642231fc2b8907d854e614710996a96f1f32018d5c",
"sha256:46515773570a33eae13e451c8fcf440222ef24bd3b26f40774dd0bd8b6db15b2",
"sha256:46f21f2600d001af10e847df9eb3b832e8a439f696c04891bcb8a8cedd859af9",
"sha256:473701599665d874919d05bb33b56180447b3a9da8d52d6d9799f381ce23f95c",
"sha256:4b9390bf973e3907d967b75be199cf1978ca8443183cf1e78ad80ad8be9cf242",
"sha256:4f415624cf8b065796649a5e4621773dc5c9ea574a944c76a7f8a6d3d2906b41",
"sha256:534032a5ceb34bba1da193b7d386ac575127cc39338379f39a164b10d97ade89",
"sha256:558485218ee06458643b929765ac1eb04519ca3d1e2dcc288517de864c747c33",
"sha256:57cf05466917e08f90e323f025b96f493f92c0344694f5702579ab4b7e2eb10d",
"sha256:59d77bfa3bea13caee95bc0d3f1c518b15049b97dd61ea8b3d71ce677a67f808",
"sha256:5d5254c815c186744c8f922e2ce861a2bdeabc06520b4b30b2f7d9767791ce6e",
"sha256:5ea121cb66d7e5cb396b4c3ca90471252b94e01809805cfe3e4e44be2db3a99c",
"sha256:60aeb14ff9022d2687ef98ce55f6342944c40d00916452bb90899a191802137a",
"sha256:642eb4cabd997c9b949a994f9643cd8ae00cf4ca8c5cd9c273962296fadf1c44",
"sha256:6548fc551de15f310dd0564751d9dc3d405278d45ea9b2b369ed1eccf142e1f5",
"sha256:68a851176c931e2b3de6214347b767451243eeed3bea34c172127bbb5bf6c210",
"sha256:6e84edecc3a82f90d44ddee2ee2a2630d4994b8471816e226d2b771cda7ac4ca",
"sha256:73e8614258404b2689a26cb5d002512b8bc4dfa18aca86382f68f959aee9b0c8",
"sha256:7679bb6e4d9a3978a46ab19a3560e8d2b7265ef3c88152e7fdc130d649789887",
"sha256:76b6c296e4f7a1a8a128aec42d128646897f9ae9a700ef6839cdc9b3900db9b5",
"sha256:7f00cc64b49d2ef19ddae898a3def9dd8fda9c3d27c8a174c2889ee757918e71",
"sha256:8021eeff7fabde21b9858ed058a8250ad230cede91764d598c2466b0ba70db8b",
"sha256:87f8f7df70b90fbe7b49969f07b347e3f978f8bd1046bb8ecae659921869202b",
"sha256:916d457ad84e05b7db52700bad0a15c56e0c3000dcaf1263b2fb7a56fe148996",
"sha256:925174cafb0f1179a7fd38da90302555d7445e34c9ece68019e53c946be7f542",
"sha256:9801bcd52ac9c795a7d81ea67471a42cffe532e46cfb750cd5713befc5c019c0",
"sha256:99cf827f5a783038eb313beee6533dddb8bdb086d7269c5c144c1c952d142ace",
"sha256:a21b78af7e2e13bec6bea12fc33bc05730197674f3e5402ce214d07026ccfebd",
"sha256:a52e8f317336a44836475e9c802f51c2dc38d612eaa76532cb1d17690338b63b",
"sha256:a702005e447d712375433ed0499cb6e1503fadd6c96a47f51d707b4d37b76d3c",
"sha256:a708c291900c40a7ecf23f1d2384ed0bc0604e24094dd13417c7e7f8f7a50d93",
"sha256:a7790a273225b0c46e5f859c1327f0f659896cc72eaa537d23aa3ad9ff2a1cc1",
"sha256:abcf7daa5ebcc89328326254f6dd6d566adb483d4d00178892afd386ab389de2",
"sha256:add017c5bd6b9ec3a5f09248396b6ee2ce61c5621f087eb2269c813cd8813808",
"sha256:af4139172ff0263d269abdcc641e944c9de4b5d660894a3ec7e9f9db63b56ac9",
"sha256:b4015baed99d046c760f09a4c59d234d8f398a454380c3cf0b859aba97136090",
"sha256:ba0006799f21d83c3717fe20e2707a10bbc296475155aadf4f5850f6659b96b9",
"sha256:bdb98f4c9e8a1735efddfaa995b0c96559792da15d56b76428bdfc29f77c4cdb",
"sha256:c34234a1bc9e466c104372af74d11a9f98338a3f72fae22b80485171a64e0144",
"sha256:c580c2a61d8297a6e47f4d01f066517dbb019be98032880d19ece7f337a9401d",
"sha256:ca9a40497f7e97a2a961c04fa8a6f23d790b0521350a8b455759d786b0bcb203",
"sha256:cab343b265e38d4e00649cbbad9278b734c5715f9bcbb72c85a1f99b1a58e19a",
"sha256:ce52aad32ec6e46d1a91ff8b8014a91538800dd533914bfc4a82f5018d971408",
"sha256:da07c7e7fc9a3f40446b78c54dbba8bfd5c9100dfecb21b65bfe3f57844f5e71",
"sha256:dc8a0dbb2a10ae8bb609584f5c504789f0f3d0d81840da4849102ec84289f952",
"sha256:e5b4b0d9440046ead3bd425eb2b852499241ee0cef1ae151038e4f87ede888c4",
"sha256:f33d8efb42e4fc2b31b3b4527940b25cdebb3026fb56a80c1c1c11a4271d2352",
"sha256:f6befb83bca720b71d6bd6326a3b26e9496ae6649e26585de024890fe50f49b8",
"sha256:fcc849b28f584ed1dbf277291ded5c32bb3476a37032df4a1d523b55faa5f944",
"sha256:ff44de36772b05c2eb74f2b4b6d1ae29b8f41ed5506310ce1258d44826ee38c1"
"sha256:0607ff0988ad7e173e5ddf7bf55ee65534bd18a5461183c33e8e41a59e89edf4",
"sha256:09b738360af8cb2da275998a8bf79517a71225b0de41ab47339c2beebfff025f",
"sha256:0a5f0e4747f31cff87d1eb32a6000bde1e603107f632ef4666be0dc065889c7a",
"sha256:0b5e96e25e70917b28a5391c2ed3ffc6156513d3db0e1476c5253fcd50f7a944",
"sha256:1104a8d47967a414a436007c52f533e933e5d52574cab407b1e49a4e9b5ddbd1",
"sha256:13dbb5c7e8f3b6a2cf6e10b0948cacb2f4c9eb05029fe31c60592d08ac63180d",
"sha256:2a906c3890da6a63224d551c2967413b8790a6357a80bf6b257c9a7978c2c42d",
"sha256:317bd63870b4d875af3c1be1b19202de34c32623609ec803b81c99193a788c1e",
"sha256:34c22eb8c819d59cec4444d9eebe2e38b95d3dcdafe08965853f8799fd71161d",
"sha256:36b16fecb10246e599f178dd74f313cbdc9f41c56e77d52100d1361eed24f51a",
"sha256:38d9759733aa04fb1697d717bfabbedb21398046bd07734be7cccc3d19ea8675",
"sha256:3e26ad9bc48d610bf6cc76c506b9e5ad9360ed7a945d9be3b5b2c8535a0145e3",
"sha256:41358bfd24425c1673f184d7c26c6ae91943fe51dfecc3603b5e08187b4bcc55",
"sha256:447d5009d6b5447b2f237395d0018901dcc673f7d9f82ba26c1b9f9c3b444b60",
"sha256:44f552e0da3c8ee3c28e2eb82b0b784200631687fc6a71277ea8ab0828780e7d",
"sha256:490712b91c65988012e866c411a40cc65b595929ececf75eeb4c79fcc3bc80a6",
"sha256:4c093c571bc3da9ebcd484e001ba18b8452903cd428c0bc926d9b0141bcb710e",
"sha256:50d3dba341f1e583265c1a808e897b4159208d814ab07530202b6036a4d86da5",
"sha256:534e946bce61fd162af02bad7bfd2daec1521b71d27238869c23a672146c34a5",
"sha256:585ea241ee4961dc18a95e2f5581dbc26285fcf330e007459688096f76be8c42",
"sha256:59e7da839a1238807226f7143c68a479dee09244d1b3cf8c134f2fce777d12d0",
"sha256:5b0f782f0e03555c55e37d93d7a57454efe7495dab33ba0ccd2dbe25fc50f05d",
"sha256:5bee1b0cbfdb87686a7fb0e46f1d8bd34d52d6932c0723a86de1cc532b1aa489",
"sha256:610807cea990fd545b1559466971649e69302c8a9472cefe1d6d48a1dee97440",
"sha256:6308062534323f0d3edb4e702a0e26a76ca9e0e23ff99be5d82750772df32a9e",
"sha256:67fa5f028e8a01e1d7944a9fb616d1d0510d5d38b0c41708310bd1bc45ae89f6",
"sha256:6a2ab9d089324d77bb81745b01f4aeffe4094306d939e92ba5e71e9a6b99b71e",
"sha256:6c198bfc169419c09b85ab10cb0f572744e686f40d1e7f4ed09061284fc1303f",
"sha256:6e56521538f19c4a6690f439fefed551f0b296bd785adc67c1777c348beb943d",
"sha256:6ec829058785d028f467be70cd195cd0aaf1a763e4d09822584ede8c9eaa4b03",
"sha256:718d7208b9c2d86aaf0294d9381a6acb0158b5ff0f3515902751404e318e02c9",
"sha256:735e3b4ce9c0616e85f302f109bdc6e425ba1670a73f962c9f6b98a6d51b77c9",
"sha256:772057fba283c095db8c8ecde4634717a35c47061d24f889468dc67190327bcd",
"sha256:7b5e2acefd33c259c4a2e157119c4373c8773cf6793e225006a1649672ab47a6",
"sha256:82d16a64236970cb93c8d63ad18c5b9f138a704331e4b916b2737ddfad14e0c4",
"sha256:87c1b0496e8c87ec9db5383e30042357b4839b46c2d556abd49ec770ce2ad868",
"sha256:8e54945dd2eeb50925500957c7c579df3cd07c29db7810b83cf30495d79af267",
"sha256:9393a05b126a7e187f3e38758255e0edf948a65b22c377414002d488221fdaa2",
"sha256:9fbc0dee7ff5f15c4428775e6fa3ed20003140560ffa22b88326669d53b3c0f4",
"sha256:a1613838aa6b89af4ba10a0f3a972836128801ed008078f8c1244e65958f1b24",
"sha256:a1bbc4efa99ed1310b5009ce7f3a1784698082ed2c1ef3895332f5df9b3b92c2",
"sha256:a555e06566c6dc167fbcd0ad507ff05fd9328502aefc963cb0a0547cfe7f00db",
"sha256:a58d78653ae422df6837dd4ca0036610b8cb4962b5cfdbd337b7b24de9e5f98a",
"sha256:a5edc58d631170de90e50adc2cc0248083541affef82f8cd93bea458e4d96db8",
"sha256:a5f623aeaa24f71fce3177d7fee875371345eb9102b355b882243e33e04b7175",
"sha256:adaab25be351fff0d8a691c4f09153647804d09a87a4e4ea2c3f9fe9e8651851",
"sha256:ade74f5e3a0fd17df5782896ddca7ddb998845a5f7cd4b0be771e1ffc3b9aa5b",
"sha256:b1d381f58fcc3e63fcc0ea4f0a38335163883267f77e4c6e22d7a30877218a0e",
"sha256:bf6005708fc2e2c89a083f258b97709559a95f9a7a03e59f805dd23c93bc3986",
"sha256:d546431636edb1d6a608b348dd58cc9841b81f4116745857b6cb9f8dadb2725f",
"sha256:d5618d49de6ba63fe4510bdada62d06a8acfca0b4b5c904956c777d28382b419",
"sha256:dfd0d464f3d86a1460683cd742306d1138b4e99b79094f4e07e1ca85ee267fe7",
"sha256:e18281a7d80d76b66a9f9e68a98cf7e1d153182772400d9a9ce855264d7d0ce7",
"sha256:e410cf3a2272d0a85526d700782a2fa92c1e304fdcc519ba74ac80b8297adf36",
"sha256:e662c6266e3a275bdcb6bb049edc7cd77d0b0f7e119a53101d367c841afc66dc",
"sha256:ec9027d0beb785a35aa9951d14e06d48cfbf876d8ff67519403a2522b181943b",
"sha256:eed394099a7792834f0cb4a8f615319152b9d801444c1c9e1b1a2c36d2239f9e",
"sha256:f76dbe44e31abf516114f6347a46fa4e7c2e8bceaa4b6f7ee3a0a03c8eba3c17",
"sha256:fc15874816b9320581133ddc2096b644582ab870cf6a6ed63684433e7af4b0d3",
"sha256:fc9fb11b65e7bc49f7f75aaba1b700f7181d95d4e151cf2f24d51bfd14410b77"
],
"index": "pypi",
"version": "==4.6.5"
"version": "==4.7.1"
},
"maxminddb": {
"hashes": [
@ -1312,12 +1312,12 @@
"version": "==0.5.0"
},
"sentry-sdk": {
"git": "https://github.com/beryju/sentry-python.git",
"hashes": [
"sha256:0db297ab32e095705c20f742c3a5dac62fe15c4318681884053d0898e5abb2f6",
"sha256:789a11a87ca02491896e121efdd64e8fd93327b69e8f2f7d42f03e2569648e88"
],
"index": "pypi",
"version": "==1.5.0"
"ref": "379aee28b15d3b87b381317746c4efd24b3d7bc3"
},
"service-identity": {
"hashes": [
@ -2387,11 +2387,11 @@
},
"tomli": {
"hashes": [
"sha256:c6ce0015eb38820eaf32b5db832dbc26deb3dd427bd5f6556cf0acac2c214fee",
"sha256:f04066f68f5554911363063a30b108d2b5a5b1a010aa8b6132af78489fe3aade"
"sha256:05b6166bff487dc068d322585c7ea4ef78deed501cc124060e0f238e89a9231f",
"sha256:e3069e4be3ead9668e21cb9b074cd948f7b3113fd9c8bba083f48247aab8b11c"
],
"markers": "python_version >= '3.6'",
"version": "==1.2.2"
"version": "==1.2.3"
},
"trio": {
"hashes": [

View File

@ -1,3 +1,3 @@
"""authentik"""
__version__ = "2021.12.1-rc4"
__version__ = "2021.12.1-rc5"
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"

View File

@ -1,13 +1,6 @@
"""authentik administration metrics"""
import time
from collections import Counter
from datetime import timedelta
from django.db.models import Count, ExpressionWrapper, F
from django.db.models.fields import DurationField
from django.db.models.functions import ExtractHour
from django.utils.timezone import now
from drf_spectacular.utils import extend_schema, extend_schema_field
from guardian.shortcuts import get_objects_for_user
from rest_framework.fields import IntegerField, SerializerMethodField
from rest_framework.permissions import IsAdminUser
from rest_framework.request import Request
@ -15,31 +8,7 @@ from rest_framework.response import Response
from rest_framework.views import APIView
from authentik.core.api.utils import PassiveSerializer
from authentik.events.models import Event, EventAction
def get_events_per_1h(**filter_kwargs) -> list[dict[str, int]]:
"""Get event count by hour in the last day, fill with zeros"""
date_from = now() - timedelta(days=1)
result = (
Event.objects.filter(created__gte=date_from, **filter_kwargs)
.annotate(age=ExpressionWrapper(now() - F("created"), output_field=DurationField()))
.annotate(age_hours=ExtractHour("age"))
.values("age_hours")
.annotate(count=Count("pk"))
.order_by("age_hours")
)
data = Counter({int(d["age_hours"]): d["count"] for d in result})
results = []
_now = now()
for hour in range(0, -24, -1):
results.append(
{
"x_cord": time.mktime((_now + timedelta(hours=hour)).timetuple()) * 1000,
"y_cord": data[hour * -1],
}
)
return results
from authentik.events.models import EventAction
class CoordinateSerializer(PassiveSerializer):
@ -58,12 +27,22 @@ class LoginMetricsSerializer(PassiveSerializer):
@extend_schema_field(CoordinateSerializer(many=True))
def get_logins_per_1h(self, _):
"""Get successful logins per hour for the last 24 hours"""
return get_events_per_1h(action=EventAction.LOGIN)
user = self.context["user"]
return (
get_objects_for_user(user, "authentik_events.view_event")
.filter(action=EventAction.LOGIN)
.get_events_per_hour()
)
@extend_schema_field(CoordinateSerializer(many=True))
def get_logins_failed_per_1h(self, _):
"""Get failed logins per hour for the last 24 hours"""
return get_events_per_1h(action=EventAction.LOGIN_FAILED)
user = self.context["user"]
return (
get_objects_for_user(user, "authentik_events.view_event")
.filter(action=EventAction.LOGIN_FAILED)
.get_events_per_hour()
)
class AdministrationMetricsViewSet(APIView):
@ -75,4 +54,5 @@ class AdministrationMetricsViewSet(APIView):
def get(self, request: Request) -> Response:
"""Login Metrics per 1h"""
serializer = LoginMetricsSerializer(True)
serializer.context["user"] = request.user
return Response(serializer.data)

View File

@ -5,6 +5,7 @@ from django.http.response import HttpResponseBadRequest
from django.shortcuts import get_object_or_404
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema
from guardian.shortcuts import get_objects_for_user
from rest_framework.decorators import action
from rest_framework.fields import ReadOnlyField
from rest_framework.parsers import MultiPartParser
@ -15,7 +16,7 @@ from rest_framework.viewsets import ModelViewSet
from rest_framework_guardian.filters import ObjectPermissionsFilter
from structlog.stdlib import get_logger
from authentik.admin.api.metrics import CoordinateSerializer, get_events_per_1h
from authentik.admin.api.metrics import CoordinateSerializer
from authentik.api.decorators import permission_required
from authentik.core.api.providers import ProviderSerializer
from authentik.core.api.used_by import UsedByMixin
@ -239,8 +240,10 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet):
"""Metrics for application logins"""
app = self.get_object()
return Response(
get_events_per_1h(
get_objects_for_user(request.user, "authentik_events.view_event")
.filter(
action=EventAction.AUTHORIZE_APPLICATION,
context__authorized_application__pk=app.pk.hex,
)
.get_events_per_hour()
)

View File

@ -104,14 +104,14 @@ class SourceViewSet(
)
matching_sources: list[UserSettingSerializer] = []
for source in _all_sources:
user_settings = source.ui_user_settings
user_settings = source.ui_user_settings()
if not user_settings:
continue
policy_engine = PolicyEngine(source, request.user, request)
policy_engine.build()
if not policy_engine.passing:
continue
source_settings = source.ui_user_settings
source_settings = source.ui_user_settings()
source_settings.initial_data["object_uid"] = source.slug
if not source_settings.is_valid():
LOGGER.warning(source_settings.errors)

View File

@ -38,7 +38,7 @@ from rest_framework.viewsets import ModelViewSet
from rest_framework_guardian.filters import ObjectPermissionsFilter
from structlog.stdlib import get_logger
from authentik.admin.api.metrics import CoordinateSerializer, get_events_per_1h
from authentik.admin.api.metrics import CoordinateSerializer
from authentik.api.decorators import permission_required
from authentik.core.api.groups import GroupSerializer
from authentik.core.api.used_by import UsedByMixin
@ -184,19 +184,31 @@ class UserMetricsSerializer(PassiveSerializer):
def get_logins_per_1h(self, _):
"""Get successful logins per hour for the last 24 hours"""
user = self.context["user"]
return get_events_per_1h(action=EventAction.LOGIN, user__pk=user.pk)
return (
get_objects_for_user(user, "authentik_events.view_event")
.filter(action=EventAction.LOGIN, user__pk=user.pk)
.get_events_per_hour()
)
@extend_schema_field(CoordinateSerializer(many=True))
def get_logins_failed_per_1h(self, _):
"""Get failed logins per hour for the last 24 hours"""
user = self.context["user"]
return get_events_per_1h(action=EventAction.LOGIN_FAILED, context__username=user.username)
return (
get_objects_for_user(user, "authentik_events.view_event")
.filter(action=EventAction.LOGIN_FAILED, context__username=user.username)
.get_events_per_hour()
)
@extend_schema_field(CoordinateSerializer(many=True))
def get_authorizations_per_1h(self, _):
"""Get failed logins per hour for the last 24 hours"""
user = self.context["user"]
return get_events_per_1h(action=EventAction.AUTHORIZE_APPLICATION, user__pk=user.pk)
return (
get_objects_for_user(user, "authentik_events.view_event")
.filter(action=EventAction.AUTHORIZE_APPLICATION, user__pk=user.pk)
.get_events_per_hour()
)
class UsersFilter(FilterSet):

View File

@ -5,6 +5,7 @@ from typing import Callable
from uuid import uuid4
from django.http import HttpRequest, HttpResponse
from sentry_sdk.api import set_tag
SESSION_IMPERSONATE_USER = "authentik_impersonate_user"
SESSION_IMPERSONATE_ORIGINAL_USER = "authentik_impersonate_original_user"
@ -50,6 +51,7 @@ class RequestIDMiddleware:
"request_id": request_id,
"host": request.get_host(),
}
set_tag("authentik.request_id", request_id)
response = self.get_response(request)
response[RESPONSE_HEADER_ID] = request.request_id
setattr(response, "ak_context", {})

View File

@ -359,13 +359,11 @@ class Source(ManagedModel, SerializerModel, PolicyBindingModel):
"""Return component used to edit this object"""
raise NotImplementedError
@property
def ui_login_button(self) -> Optional[UILoginButton]:
def ui_login_button(self, request: HttpRequest) -> Optional[UILoginButton]:
"""If source uses a http-based flow, return UI Information about the login
button. If source doesn't use http-based flow, return None."""
return None
@property
def ui_user_settings(self) -> Optional[UserSettingSerializer]:
"""Entrypoint to integrate with User settings. Can either return None if no
user settings are available, or UserSettingSerializer."""

View File

@ -19,6 +19,7 @@
<script src="{% static 'dist/poly.js' %}" type="module"></script>
{% block head %}
{% endblock %}
<meta name="sentry-trace" content="{{ sentry_trace }}" />
</head>
<body>
{% block body %}

View File

@ -2,7 +2,7 @@
from time import sleep
from typing import Callable, Type
from django.test import TestCase
from django.test import RequestFactory, TestCase
from django.utils.timezone import now
from guardian.shortcuts import get_anonymous_user
@ -30,6 +30,9 @@ class TestModels(TestCase):
def source_tester_factory(test_model: Type[Stage]) -> Callable:
"""Test source"""
factory = RequestFactory()
request = factory.get("/")
def tester(self: TestModels):
model_class = None
if test_model._meta.abstract:
@ -38,8 +41,8 @@ def source_tester_factory(test_model: Type[Stage]) -> Callable:
model_class = test_model()
model_class.slug = "test"
self.assertIsNotNone(model_class.component)
_ = model_class.ui_login_button
_ = model_class.ui_user_settings
_ = model_class.ui_login_button(request)
_ = model_class.ui_user_settings()
return tester

View File

@ -41,7 +41,7 @@ class TestPropertyMappingAPI(APITestCase):
expr = "return True"
self.assertEqual(PropertyMappingSerializer().validate_expression(expr), expr)
with self.assertRaises(ValidationError):
print(PropertyMappingSerializer().validate_expression("/"))
PropertyMappingSerializer().validate_expression("/")
def test_types(self):
"""Test PropertyMappigns's types endpoint"""

View File

@ -11,10 +11,13 @@ from cryptography.hazmat.primitives.serialization import load_pem_private_key
from cryptography.x509 import Certificate, load_pem_x509_certificate
from django.db import models
from django.utils.translation import gettext_lazy as _
from structlog.stdlib import get_logger
from authentik.lib.models import CreatedUpdatedModel
from authentik.managed.models import ManagedModel
LOGGER = get_logger()
class CertificateKeyPair(ManagedModel, CreatedUpdatedModel):
"""CertificateKeyPair that can be used for signing or encrypting if `key_data`
@ -62,7 +65,8 @@ class CertificateKeyPair(ManagedModel, CreatedUpdatedModel):
password=None,
backend=default_backend(),
)
except ValueError:
except ValueError as exc:
LOGGER.warning(exc)
return None
return self._private_key

View File

@ -2,6 +2,9 @@
from glob import glob
from pathlib import Path
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.serialization import load_pem_private_key
from cryptography.x509.base import load_pem_x509_certificate
from django.utils.translation import gettext_lazy as _
from structlog.stdlib import get_logger
@ -20,6 +23,22 @@ LOGGER = get_logger()
MANAGED_DISCOVERED = "goauthentik.io/crypto/discovered/%s"
def ensure_private_key_valid(body: str):
"""Attempt loading of an RSA Private key without password"""
load_pem_private_key(
str.encode("\n".join([x.strip() for x in body.split("\n")])),
password=None,
backend=default_backend(),
)
return body
def ensure_certificate_valid(body: str):
"""Attempt loading of a PEM-encoded certificate"""
load_pem_x509_certificate(body.encode("utf-8"), default_backend())
return body
@CELERY_APP.task(bind=True, base=MonitoredTask)
@prefill_task
def certificate_discovery(self: MonitoredTask):
@ -42,11 +61,11 @@ def certificate_discovery(self: MonitoredTask):
with open(path, "r+", encoding="utf-8") as _file:
body = _file.read()
if "BEGIN RSA PRIVATE KEY" in body:
private_keys[cert_name] = body
private_keys[cert_name] = ensure_private_key_valid(body)
else:
certs[cert_name] = body
except OSError as exc:
LOGGER.warning("Failed to open file", exc=exc, file=path)
certs[cert_name] = ensure_certificate_valid(body)
except (OSError, ValueError) as exc:
LOGGER.warning("Failed to open file or invalid format", exc=exc, file=path)
discovered += 1
for name, cert_data in certs.items():
cert = CertificateKeyPair.objects.filter(managed=MANAGED_DISCOVERED % name).first()

View File

@ -1,4 +1,6 @@
"""Events API Views"""
from json import loads
import django_filters
from django.db.models.aggregates import Count
from django.db.models.fields.json import KeyTextTransform
@ -12,6 +14,7 @@ from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
from authentik.admin.api.metrics import CoordinateSerializer
from authentik.core.api.utils import PassiveSerializer, TypeCreateSerializer
from authentik.events.models import Event, EventAction
@ -110,13 +113,20 @@ class EventViewSet(ModelViewSet):
@extend_schema(
methods=["GET"],
responses={200: EventTopPerUserSerializer(many=True)},
filters=[],
parameters=[
OpenApiParameter(
"action",
type=OpenApiTypes.STR,
location=OpenApiParameter.QUERY,
required=False,
),
OpenApiParameter(
"top_n",
type=OpenApiTypes.INT,
location=OpenApiParameter.QUERY,
required=False,
)
),
],
)
@action(detail=False, methods=["GET"], pagination_class=None)
@ -137,6 +147,40 @@ class EventViewSet(ModelViewSet):
.order_by("-counted_events")[:top_n]
)
@extend_schema(
methods=["GET"],
responses={200: CoordinateSerializer(many=True)},
filters=[],
parameters=[
OpenApiParameter(
"action",
type=OpenApiTypes.STR,
location=OpenApiParameter.QUERY,
required=False,
),
OpenApiParameter(
"query",
type=OpenApiTypes.STR,
location=OpenApiParameter.QUERY,
required=False,
),
],
)
@action(detail=False, methods=["GET"], pagination_class=None)
def per_month(self, request: Request):
"""Get the count of events per month"""
filtered_action = request.query_params.get("action", EventAction.LOGIN)
try:
query = loads(request.query_params.get("query", "{}"))
except ValueError:
return Response(status=400)
return Response(
get_objects_for_user(request.user, "authentik_events.view_event")
.filter(action=filtered_action)
.filter(**query)
.get_events_per_day()
)
@extend_schema(responses={200: TypeCreateSerializer(many=True)})
@action(detail=False, pagination_class=None, filter_backends=[])
def actions(self, request: Request) -> Response:

View File

@ -7,6 +7,7 @@ from typing import Optional, TypedDict
from geoip2.database import Reader
from geoip2.errors import GeoIP2Error
from geoip2.models import City
from sentry_sdk.hub import Hub
from structlog.stdlib import get_logger
from authentik.lib.config import CONFIG
@ -62,13 +63,17 @@ class GeoIPReader:
def city(self, ip_address: str) -> Optional[City]:
"""Wrapper for Reader.city"""
if not self.enabled:
return None
self.__check_expired()
try:
return self.__reader.city(ip_address)
except (GeoIP2Error, ValueError):
return None
with Hub.current.start_span(
op="authentik.events.geo.city",
description=ip_address,
):
if not self.enabled:
return None
self.__check_expired()
try:
return self.__reader.city(ip_address)
except (GeoIP2Error, ValueError):
return None
def city_dict(self, ip_address: str) -> Optional[GeoIPDict]:
"""Wrapper for self.city that returns a dict"""

View File

@ -314,169 +314,10 @@ class Migration(migrations.Migration):
old_name="user_json",
new_name="user",
),
migrations.AlterField(
model_name="event",
name="action",
field=models.TextField(
choices=[
("login", "Login"),
("login_failed", "Login Failed"),
("logout", "Logout"),
("sign_up", "Sign Up"),
("authorize_application", "Authorize Application"),
("suspicious_request", "Suspicious Request"),
("password_set", "Password Set"),
("invitation_created", "Invite Created"),
("invitation_used", "Invite Used"),
("source_linked", "Source Linked"),
("impersonation_started", "Impersonation Started"),
("impersonation_ended", "Impersonation Ended"),
("model_created", "Model Created"),
("model_updated", "Model Updated"),
("model_deleted", "Model Deleted"),
("custom_", "Custom Prefix"),
]
),
),
migrations.AlterField(
model_name="event",
name="action",
field=models.TextField(
choices=[
("login", "Login"),
("login_failed", "Login Failed"),
("logout", "Logout"),
("user_write", "User Write"),
("suspicious_request", "Suspicious Request"),
("password_set", "Password Set"),
("invitation_created", "Invite Created"),
("invitation_used", "Invite Used"),
("authorize_application", "Authorize Application"),
("source_linked", "Source Linked"),
("impersonation_started", "Impersonation Started"),
("impersonation_ended", "Impersonation Ended"),
("model_created", "Model Created"),
("model_updated", "Model Updated"),
("model_deleted", "Model Deleted"),
("custom_", "Custom Prefix"),
]
),
),
migrations.RemoveField(
model_name="event",
name="date",
),
migrations.AlterField(
model_name="event",
name="action",
field=models.TextField(
choices=[
("login", "Login"),
("login_failed", "Login Failed"),
("logout", "Logout"),
("user_write", "User Write"),
("suspicious_request", "Suspicious Request"),
("password_set", "Password Set"),
("token_view", "Token View"),
("invitation_created", "Invite Created"),
("invitation_used", "Invite Used"),
("authorize_application", "Authorize Application"),
("source_linked", "Source Linked"),
("impersonation_started", "Impersonation Started"),
("impersonation_ended", "Impersonation Ended"),
("model_created", "Model Created"),
("model_updated", "Model Updated"),
("model_deleted", "Model Deleted"),
("custom_", "Custom Prefix"),
]
),
),
migrations.AlterField(
model_name="event",
name="action",
field=models.TextField(
choices=[
("login", "Login"),
("login_failed", "Login Failed"),
("logout", "Logout"),
("user_write", "User Write"),
("suspicious_request", "Suspicious Request"),
("password_set", "Password Set"),
("token_view", "Token View"),
("invitation_created", "Invite Created"),
("invitation_used", "Invite Used"),
("authorize_application", "Authorize Application"),
("source_linked", "Source Linked"),
("impersonation_started", "Impersonation Started"),
("impersonation_ended", "Impersonation Ended"),
("policy_execution", "Policy Execution"),
("policy_exception", "Policy Exception"),
("property_mapping_exception", "Property Mapping Exception"),
("model_created", "Model Created"),
("model_updated", "Model Updated"),
("model_deleted", "Model Deleted"),
("custom_", "Custom Prefix"),
]
),
),
migrations.AlterField(
model_name="event",
name="action",
field=models.TextField(
choices=[
("login", "Login"),
("login_failed", "Login Failed"),
("logout", "Logout"),
("user_write", "User Write"),
("suspicious_request", "Suspicious Request"),
("password_set", "Password Set"),
("token_view", "Token View"),
("invitation_created", "Invite Created"),
("invitation_used", "Invite Used"),
("authorize_application", "Authorize Application"),
("source_linked", "Source Linked"),
("impersonation_started", "Impersonation Started"),
("impersonation_ended", "Impersonation Ended"),
("policy_execution", "Policy Execution"),
("policy_exception", "Policy Exception"),
("property_mapping_exception", "Property Mapping Exception"),
("model_created", "Model Created"),
("model_updated", "Model Updated"),
("model_deleted", "Model Deleted"),
("update_available", "Update Available"),
("custom_", "Custom Prefix"),
]
),
),
migrations.AlterField(
model_name="event",
name="action",
field=models.TextField(
choices=[
("login", "Login"),
("login_failed", "Login Failed"),
("logout", "Logout"),
("user_write", "User Write"),
("suspicious_request", "Suspicious Request"),
("password_set", "Password Set"),
("token_view", "Token View"),
("invitation_used", "Invite Used"),
("authorize_application", "Authorize Application"),
("source_linked", "Source Linked"),
("impersonation_started", "Impersonation Started"),
("impersonation_ended", "Impersonation Ended"),
("policy_execution", "Policy Execution"),
("policy_exception", "Policy Exception"),
("property_mapping_exception", "Property Mapping Exception"),
("configuration_error", "Configuration Error"),
("model_created", "Model Created"),
("model_updated", "Model Updated"),
("model_deleted", "Model Deleted"),
("update_available", "Update Available"),
("custom_", "Custom Prefix"),
]
),
),
migrations.CreateModel(
name="NotificationTransport",
fields=[
@ -610,68 +451,6 @@ class Migration(migrations.Migration):
help_text="Only send notification once, for example when sending a webhook into a chat channel.",
),
),
migrations.AlterField(
model_name="event",
name="action",
field=models.TextField(
choices=[
("login", "Login"),
("login_failed", "Login Failed"),
("logout", "Logout"),
("user_write", "User Write"),
("suspicious_request", "Suspicious Request"),
("password_set", "Password Set"),
("token_view", "Token View"),
("invitation_used", "Invite Used"),
("authorize_application", "Authorize Application"),
("source_linked", "Source Linked"),
("impersonation_started", "Impersonation Started"),
("impersonation_ended", "Impersonation Ended"),
("policy_execution", "Policy Execution"),
("policy_exception", "Policy Exception"),
("property_mapping_exception", "Property Mapping Exception"),
("system_task_execution", "System Task Execution"),
("system_task_exception", "System Task Exception"),
("configuration_error", "Configuration Error"),
("model_created", "Model Created"),
("model_updated", "Model Updated"),
("model_deleted", "Model Deleted"),
("update_available", "Update Available"),
("custom_", "Custom Prefix"),
]
),
),
migrations.AlterField(
model_name="event",
name="action",
field=models.TextField(
choices=[
("login", "Login"),
("login_failed", "Login Failed"),
("logout", "Logout"),
("user_write", "User Write"),
("suspicious_request", "Suspicious Request"),
("password_set", "Password Set"),
("secret_view", "Secret View"),
("invitation_used", "Invite Used"),
("authorize_application", "Authorize Application"),
("source_linked", "Source Linked"),
("impersonation_started", "Impersonation Started"),
("impersonation_ended", "Impersonation Ended"),
("policy_execution", "Policy Execution"),
("policy_exception", "Policy Exception"),
("property_mapping_exception", "Property Mapping Exception"),
("system_task_execution", "System Task Execution"),
("system_task_exception", "System Task Exception"),
("configuration_error", "Configuration Error"),
("model_created", "Model Created"),
("model_updated", "Model Updated"),
("model_deleted", "Model Deleted"),
("update_available", "Update Available"),
("custom_", "Custom Prefix"),
]
),
),
migrations.RunPython(
code=token_view_to_secret_view,
),
@ -688,76 +467,11 @@ class Migration(migrations.Migration):
migrations.RunPython(
code=update_expires,
),
migrations.AlterField(
model_name="event",
name="action",
field=models.TextField(
choices=[
("login", "Login"),
("login_failed", "Login Failed"),
("logout", "Logout"),
("user_write", "User Write"),
("suspicious_request", "Suspicious Request"),
("password_set", "Password Set"),
("secret_view", "Secret View"),
("invitation_used", "Invite Used"),
("authorize_application", "Authorize Application"),
("source_linked", "Source Linked"),
("impersonation_started", "Impersonation Started"),
("impersonation_ended", "Impersonation Ended"),
("policy_execution", "Policy Execution"),
("policy_exception", "Policy Exception"),
("property_mapping_exception", "Property Mapping Exception"),
("system_task_execution", "System Task Execution"),
("system_task_exception", "System Task Exception"),
("configuration_error", "Configuration Error"),
("model_created", "Model Created"),
("model_updated", "Model Updated"),
("model_deleted", "Model Deleted"),
("email_sent", "Email Sent"),
("update_available", "Update Available"),
("custom_", "Custom Prefix"),
]
),
),
migrations.AddField(
model_name="event",
name="tenant",
field=models.JSONField(blank=True, default=authentik.events.models.default_tenant),
),
migrations.AlterField(
model_name="event",
name="action",
field=models.TextField(
choices=[
("login", "Login"),
("login_failed", "Login Failed"),
("logout", "Logout"),
("user_write", "User Write"),
("suspicious_request", "Suspicious Request"),
("password_set", "Password Set"),
("secret_view", "Secret View"),
("invitation_used", "Invite Used"),
("authorize_application", "Authorize Application"),
("source_linked", "Source Linked"),
("impersonation_started", "Impersonation Started"),
("impersonation_ended", "Impersonation Ended"),
("policy_execution", "Policy Execution"),
("policy_exception", "Policy Exception"),
("property_mapping_exception", "Property Mapping Exception"),
("system_task_execution", "System Task Execution"),
("system_task_exception", "System Task Exception"),
("system_exception", "System Exception"),
("configuration_error", "Configuration Error"),
("model_created", "Model Created"),
("model_updated", "Model Updated"),
("model_deleted", "Model Deleted"),
("email_sent", "Email Sent"),
("update_available", "Update Available"),
("custom_", "Custom Prefix"),
]
),
),
migrations.AlterField(
model_name="event",
name="action",
@ -776,6 +490,7 @@ class Migration(migrations.Migration):
("source_linked", "Source Linked"),
("impersonation_started", "Impersonation Started"),
("impersonation_ended", "Impersonation Ended"),
("flow_execution", "Flow Execution"),
("policy_execution", "Policy Execution"),
("policy_exception", "Policy Exception"),
("property_mapping_exception", "Property Mapping Exception"),

View File

@ -1,4 +1,6 @@
"""authentik events models"""
import time
from collections import Counter
from datetime import timedelta
from inspect import getmodule, stack
from smtplib import SMTPException
@ -7,6 +9,12 @@ from uuid import uuid4
from django.conf import settings
from django.db import models
from django.db.models import Count, ExpressionWrapper, F
from django.db.models.fields import DurationField
from django.db.models.functions import ExtractHour
from django.db.models.functions.datetime import ExtractDay
from django.db.models.manager import Manager
from django.db.models.query import QuerySet
from django.http import HttpRequest
from django.http.request import QueryDict
from django.utils.timezone import now
@ -70,6 +78,7 @@ class EventAction(models.TextChoices):
IMPERSONATION_STARTED = "impersonation_started"
IMPERSONATION_ENDED = "impersonation_ended"
FLOW_EXECUTION = "flow_execution"
POLICY_EXECUTION = "policy_execution"
POLICY_EXCEPTION = "policy_exception"
PROPERTY_MAPPING_EXCEPTION = "property_mapping_exception"
@ -90,6 +99,72 @@ class EventAction(models.TextChoices):
CUSTOM_PREFIX = "custom_"
class EventQuerySet(QuerySet):
"""Custom events query set with helper functions"""
def get_events_per_hour(self) -> list[dict[str, int]]:
"""Get event count by hour in the last day, fill with zeros"""
date_from = now() - timedelta(days=1)
result = (
self.filter(created__gte=date_from)
.annotate(age=ExpressionWrapper(now() - F("created"), output_field=DurationField()))
.annotate(age_hours=ExtractHour("age"))
.values("age_hours")
.annotate(count=Count("pk"))
.order_by("age_hours")
)
data = Counter({int(d["age_hours"]): d["count"] for d in result})
results = []
_now = now()
for hour in range(0, -24, -1):
results.append(
{
"x_cord": time.mktime((_now + timedelta(hours=hour)).timetuple()) * 1000,
"y_cord": data[hour * -1],
}
)
return results
def get_events_per_day(self) -> list[dict[str, int]]:
"""Get event count by hour in the last day, fill with zeros"""
date_from = now() - timedelta(weeks=4)
result = (
self.filter(created__gte=date_from)
.annotate(age=ExpressionWrapper(now() - F("created"), output_field=DurationField()))
.annotate(age_days=ExtractDay("age"))
.values("age_days")
.annotate(count=Count("pk"))
.order_by("age_days")
)
data = Counter({int(d["age_days"]): d["count"] for d in result})
results = []
_now = now()
for day in range(0, -30, -1):
results.append(
{
"x_cord": time.mktime((_now + timedelta(days=day)).timetuple()) * 1000,
"y_cord": data[day * -1],
}
)
return results
class EventManager(Manager):
"""Custom helper methods for Events"""
def get_queryset(self) -> QuerySet:
"""use custom queryset"""
return EventQuerySet(self.model, using=self._db)
def get_events_per_hour(self) -> list[dict[str, int]]:
"""Wrap method from queryset"""
return self.get_queryset().get_events_per_hour()
def get_events_per_day(self) -> list[dict[str, int]]:
"""Wrap method from queryset"""
return self.get_queryset().get_events_per_day()
class Event(ExpiringModel):
"""An individual Audit/Metrics/Notification/Error Event"""
@ -105,6 +180,8 @@ class Event(ExpiringModel):
# Shadow the expires attribute from ExpiringModel to override the default duration
expires = models.DateTimeField(default=default_event_duration)
objects = EventManager()
@staticmethod
def _get_app_from_request(request: HttpRequest) -> str:
if not isinstance(request, HttpRequest):

View File

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

View File

@ -90,7 +90,7 @@ class StageViewSet(
stages += list(configurable_stage.objects.all().order_by("name"))
matching_stages: list[dict] = []
for stage in stages:
user_settings = stage.ui_user_settings
user_settings = stage.ui_user_settings()
if not user_settings:
continue
user_settings.initial_data["object_uid"] = str(stage.pk)

View File

@ -75,7 +75,6 @@ class Stage(SerializerModel):
"""Return component used to edit this object"""
raise NotImplementedError
@property
def ui_user_settings(self) -> Optional[UserSettingSerializer]:
"""Entrypoint to integrate with User settings. Can either return None if no
user settings are available, or a challenge."""

View File

@ -126,7 +126,9 @@ class FlowPlanner:
) -> FlowPlan:
"""Check each of the flows' policies, check policies for each stage with PolicyBinding
and return ordered list"""
with Hub.current.start_span(op="flow.planner.plan", description=self.flow.slug) as span:
with Hub.current.start_span(
op="authentik.flow.planner.plan", description=self.flow.slug
) as span:
span: Span
span.set_data("flow", self.flow)
span.set_data("request", request)
@ -181,7 +183,7 @@ class FlowPlanner:
"""Build flow plan by checking each stage in their respective
order and checking the applied policies"""
with Hub.current.start_span(
op="flow.planner.build_plan",
op="authentik.flow.planner.build_plan",
description=self.flow.slug,
) as span, HIST_FLOWS_PLAN_TIME.labels(flow_slug=self.flow.slug).time():
span: Span

View File

@ -6,6 +6,7 @@ from django.http.response import HttpResponse
from django.urls import reverse
from django.views.generic.base import View
from rest_framework.request import Request
from sentry_sdk.hub import Hub
from structlog.stdlib import get_logger
from authentik.core.models import DEFAULT_AVATAR, User
@ -94,8 +95,16 @@ class ChallengeStageView(StageView):
keep_context=keep_context,
)
return self.executor.restart_flow(keep_context)
return self.challenge_invalid(challenge)
return self.challenge_valid(challenge)
with Hub.current.start_span(
op="authentik.flow.stage.challenge_invalid",
description=self.__class__.__name__,
):
return self.challenge_invalid(challenge)
with Hub.current.start_span(
op="authentik.flow.stage.challenge_valid",
description=self.__class__.__name__,
):
return self.challenge_valid(challenge)
def format_title(self) -> str:
"""Allow usage of placeholder in flow title."""
@ -104,7 +113,11 @@ class ChallengeStageView(StageView):
}
def _get_challenge(self, *args, **kwargs) -> Challenge:
challenge = self.get_challenge(*args, **kwargs)
with Hub.current.start_span(
op="authentik.flow.stage.get_challenge",
description=self.__class__.__name__,
):
challenge = self.get_challenge(*args, **kwargs)
if "flow_info" not in challenge.initial_data:
flow_info = ContextualFlowInfo(
data={

View File

@ -32,7 +32,7 @@ class TestFlowsAPI(APITestCase):
def test_models(self):
"""Test that ui_user_settings returns none"""
self.assertIsNone(Stage().ui_user_settings)
self.assertIsNone(Stage().ui_user_settings())
def test_api_serializer(self):
"""Test that stage serializer returns the correct type"""

View File

@ -23,7 +23,7 @@ def model_tester_factory(test_model: Type[Stage]) -> Callable:
model_class = test_model()
self.assertTrue(issubclass(model_class.type, StageView))
self.assertIsNotNone(test_model.component)
_ = model_class.ui_user_settings
_ = model_class.ui_user_settings()
return tester

View File

@ -160,7 +160,7 @@ class FlowExecutorView(APIView):
# pylint: disable=unused-argument, too-many-return-statements
def dispatch(self, request: HttpRequest, flow_slug: str) -> HttpResponse:
with Hub.current.start_span(
op="flow.executor.dispatch", description=self.flow.slug
op="authentik.flow.executor.dispatch", description=self.flow.slug
) as span:
span.set_data("authentik Flow", self.flow.slug)
get_params = QueryDict(request.GET.get("query", ""))
@ -275,7 +275,7 @@ class FlowExecutorView(APIView):
)
try:
with Hub.current.start_span(
op="flow.executor.stage",
op="authentik.flow.executor.stage",
description=class_to_path(self.current_stage_view.__class__),
) as span:
span.set_data("Method", "GET")
@ -319,7 +319,7 @@ class FlowExecutorView(APIView):
)
try:
with Hub.current.start_span(
op="flow.executor.stage",
op="authentik.flow.executor.stage",
description=class_to_path(self.current_stage_view.__class__),
) as span:
span.set_data("Method", "POST")
@ -371,6 +371,12 @@ class FlowExecutorView(APIView):
NEXT_ARG_NAME, "authentik_core:root-redirect"
)
self.cancel()
Event.new(
action=EventAction.FLOW_EXECUTION,
flow=self.flow,
designation=self.flow.designation,
successful=True,
).from_http(self.request)
return to_stage_response(self.request, redirect_with_qs(next_param))
def stage_ok(self) -> HttpResponse:

View File

@ -106,7 +106,7 @@ class FlowInspectorView(APIView):
else:
try:
current_plan = request.session[SESSION_KEY_HISTORY][-1]
except KeyError:
except IndexError:
return Response(status=400)
is_completed = True
current_serializer = FlowInspectorPlanSerializer(

View File

@ -80,8 +80,9 @@ class BaseEvaluator:
"""Parse and evaluate expression. If the syntax is incorrect, a SyntaxError is raised.
If any exception is raised during execution, it is raised.
The result is returned without any type-checking."""
with Hub.current.start_span(op="lib.evaluator.evaluate") as span:
with Hub.current.start_span(op="authentik.lib.evaluator.evaluate") as span:
span: Span
span.description = self._filename
span.set_data("expression", expression_source)
param_keys = self._context.keys()
try:

View File

@ -90,7 +90,7 @@ class PolicyEngine:
def build(self) -> "PolicyEngine":
"""Build wrapper which monitors performance"""
with Hub.current.start_span(
op="policy.engine.build",
op="authentik.policy.engine.build",
description=self.__pbm,
) as span, HIST_POLICIES_BUILD_TIME.labels(
object_name=self.__pbm,

View File

@ -66,6 +66,7 @@ class Migration(migrations.Migration):
("source_linked", "Source Linked"),
("impersonation_started", "Impersonation Started"),
("impersonation_ended", "Impersonation Ended"),
("flow_execution", "Flow Execution"),
("policy_execution", "Policy Execution"),
("policy_exception", "Policy Exception"),
("property_mapping_exception", "Property Mapping Exception"),

View File

@ -74,4 +74,4 @@ class TestExpressionPolicyAPI(APITestCase):
expr = "return True"
self.assertEqual(ExpressionPolicySerializer().validate_expression(expr), expr)
with self.assertRaises(ValidationError):
print(ExpressionPolicySerializer().validate_expression("/"))
ExpressionPolicySerializer().validate_expression("/")

View File

@ -130,7 +130,7 @@ class PolicyProcess(PROCESS_CLASS):
def profiling_wrapper(self):
"""Run with profiling enabled"""
with Hub.current.start_span(
op="policy.process.execute",
op="authentik.policy.process.execute",
) as span, HIST_POLICIES_EXECUTION_TIME.labels(
binding_order=self.binding.order,
binding_target_type=self.binding.target_type,

View File

@ -8,7 +8,6 @@ from datetime import datetime
from hashlib import sha256
from typing import Any, Optional, Type
from urllib.parse import urlparse
from uuid import uuid4
from dacite import from_dict
from django.db import models
@ -225,7 +224,7 @@ class OAuth2Provider(Provider):
token = RefreshToken(
user=user,
provider=self,
refresh_token=uuid4().hex,
refresh_token=generate_key(),
expires=timezone.now() + timedelta_from_string(self.token_validity),
scope=scope,
)
@ -434,7 +433,7 @@ class RefreshToken(ExpiringModel, BaseGrantModel):
"""Create access token with a similar format as Okta, Keycloak, ADFS"""
token = self.create_id_token(user, request).to_dict()
token["cid"] = self.provider.client_id
token["uid"] = uuid4().hex
token["uid"] = generate_key()
return self.provider.encode(token)
def create_id_token(self, user: User, request: HttpRequest) -> IDToken:

View File

@ -194,8 +194,10 @@ class TokenView(View):
self.params = TokenParams.parse(request, self.provider, client_id, client_secret)
if self.params.grant_type == GRANT_TYPE_AUTHORIZATION_CODE:
LOGGER.info("Converting authorization code to refresh token")
return TokenResponse(self.create_code_response())
if self.params.grant_type == GRANT_TYPE_REFRESH_TOKEN:
LOGGER.info("Refreshing refresh token")
return TokenResponse(self.create_refresh_response())
raise ValueError(f"Invalid grant_type: {self.params.grant_type}")
except TokenError as error:

View File

@ -70,13 +70,14 @@ class AssertionProcessor:
"""Get AttributeStatement Element with Attributes from Property Mappings."""
# https://commons.lbl.gov/display/IDMgmt/Attribute+Definitions
attribute_statement = Element(f"{{{NS_SAML_ASSERTION}}}AttributeStatement")
user = self.http_request.user
for mapping in self.provider.property_mappings.all().select_subclasses():
if not isinstance(mapping, SAMLPropertyMapping):
continue
try:
mapping: SAMLPropertyMapping
value = mapping.evaluate(
user=self.http_request.user,
user=user,
request=self.http_request,
provider=self.provider,
)

View File

@ -1,7 +1,6 @@
"""Source API Views"""
from typing import Any
from django.utils.text import slugify
from django_filters.filters import AllValuesMultipleFilter
from django_filters.filterset import FilterSet
from drf_spectacular.types import OpenApiTypes
@ -110,7 +109,8 @@ class LDAPSourceViewSet(UsedByMixin, ModelViewSet):
GroupLDAPSynchronizer,
MembershipLDAPSynchronizer,
]:
task = TaskInfo.by_name(f"ldap_sync_{slugify(source.name)}-{sync_class.__name__}")
sync_name = sync_class.__name__.replace("LDAPSynchronizer", "").lower()
task = TaskInfo.by_name(f"ldap_sync_{source.slug}_{sync_name}")
if task:
results.append(task)
return Response(TaskSerializer(results, many=True).data)

View File

@ -29,7 +29,7 @@ class GroupLDAPSynchronizer(BaseLDAPSynchronizer):
group_dn = self._flatten(self._flatten(group.get("entryDN", group.get("dn"))))
if self._source.object_uniqueness_field not in attributes:
self.message(
f"Cannot find uniqueness field in attributes: '{group_dn}",
f"Cannot find uniqueness field in attributes: '{group_dn}'",
attributes=attributes.keys(),
dn=group_dn,
)

View File

@ -31,7 +31,7 @@ class UserLDAPSynchronizer(BaseLDAPSynchronizer):
user_dn = self._flatten(user.get("entryDN", user.get("dn")))
if self._source.object_uniqueness_field not in attributes:
self.message(
f"Cannot find uniqueness field in attributes: '{user_dn}",
f"Cannot find uniqueness field in attributes: '{user_dn}'",
attributes=attributes.keys(),
dn=user_dn,
)

View File

@ -1,5 +1,4 @@
"""LDAP Sync tasks"""
from django.utils.text import slugify
from ldap3.core.exceptions import LDAPException
from structlog.stdlib import get_logger
@ -39,7 +38,7 @@ def ldap_sync(self: MonitoredTask, source_pk: str, sync_class: str):
# to set the state with
return
sync = path_to_class(sync_class)
self.set_uid(f"{slugify(source.name)}_{sync.__name__.replace('LDAPSynchronizer', '').lower()}")
self.set_uid(f"{source.slug}_{sync.__name__.replace('LDAPSynchronizer', '').lower()}")
try:
sync_inst = sync(source)
count = sync_inst.sync()

View File

@ -14,6 +14,7 @@ AUTHENTIK_SOURCES_OAUTH_TYPES = [
"authentik.sources.oauth.types.github",
"authentik.sources.oauth.types.google",
"authentik.sources.oauth.types.oidc",
"authentik.sources.oauth.types.okta",
"authentik.sources.oauth.types.reddit",
"authentik.sources.oauth.types.twitter",
]

View File

@ -2,13 +2,13 @@
from typing import TYPE_CHECKING, Optional, Type
from django.db import models
from django.http.request import HttpRequest
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from rest_framework.serializers import Serializer
from authentik.core.models import Source, UserSourceConnection
from authentik.core.types import UILoginButton, UserSettingSerializer
from authentik.flows.challenge import ChallengeTypes, RedirectChallenge
if TYPE_CHECKING:
from authentik.sources.oauth.types.manager import SourceType
@ -64,24 +64,15 @@ class OAuthSource(Source):
return OAuthSourceSerializer
@property
def ui_login_button(self) -> UILoginButton:
def ui_login_button(self, request: HttpRequest) -> UILoginButton:
provider_type = self.type
provider = provider_type()
return UILoginButton(
challenge=RedirectChallenge(
instance={
"type": ChallengeTypes.REDIRECT.value,
"to": reverse(
"authentik_sources_oauth:oauth-client-login",
kwargs={"source_slug": self.slug},
),
}
),
icon_url=provider_type().icon_url(),
name=self.name,
icon_url=provider.icon_url(),
challenge=provider.login_challenge(self, request),
)
@property
def ui_user_settings(self) -> Optional[UserSettingSerializer]:
return UserSettingSerializer(
data={
@ -183,6 +174,16 @@ class AppleOAuthSource(OAuthSource):
verbose_name_plural = _("Apple OAuth Sources")
class OktaOAuthSource(OAuthSource):
"""Login using a okta.com."""
class Meta:
abstract = True
verbose_name = _("Okta OAuth Source")
verbose_name_plural = _("Okta OAuth Sources")
class UserOAuthSourceConnection(UserSourceConnection):
"""Authorized remote OAuth provider."""

View File

@ -2,10 +2,15 @@
from time import time
from typing import Any, Optional
from django.http.request import HttpRequest
from django.urls.base import reverse
from jwt import decode, encode
from rest_framework.fields import CharField
from structlog.stdlib import get_logger
from authentik.flows.challenge import Challenge, ChallengeResponse, ChallengeTypes
from authentik.sources.oauth.clients.oauth2 import OAuth2Client
from authentik.sources.oauth.models import OAuthSource
from authentik.sources.oauth.types.manager import MANAGER, SourceType
from authentik.sources.oauth.views.callback import OAuthCallback
from authentik.sources.oauth.views.redirect import OAuthRedirect
@ -13,18 +18,34 @@ from authentik.sources.oauth.views.redirect import OAuthRedirect
LOGGER = get_logger()
class AppleLoginChallenge(Challenge):
"""Special challenge for apple-native authentication flow, which happens on the client."""
client_id = CharField()
component = CharField(default="ak-flow-sources-oauth-apple")
scope = CharField()
redirect_uri = CharField()
state = CharField()
class AppleChallengeResponse(ChallengeResponse):
"""Pseudo class for plex response"""
component = CharField(default="ak-flow-sources-oauth-apple")
class AppleOAuthClient(OAuth2Client):
"""Apple OAuth2 client"""
def get_client_id(self) -> str:
parts = self.source.consumer_key.split(";")
parts: list[str] = self.source.consumer_key.split(";")
if len(parts) < 3:
return self.source.consumer_key
return parts[0]
return parts[0].strip()
def get_client_secret(self) -> str:
now = time()
parts = self.source.consumer_key.split(";")
parts: list[str] = self.source.consumer_key.split(";")
if len(parts) < 3:
raise ValueError(
(
@ -34,14 +55,14 @@ class AppleOAuthClient(OAuth2Client):
)
LOGGER.debug("got values from client_id", team=parts[1], kid=parts[2])
payload = {
"iss": parts[1],
"iss": parts[1].strip(),
"iat": now,
"exp": now + 86400 * 180,
"aud": "https://appleid.apple.com",
"sub": parts[0],
"sub": parts[0].strip(),
}
# pyright: reportGeneralTypeIssues=false
jwt = encode(payload, self.source.consumer_secret, "ES256", {"kid": parts[2]})
jwt = encode(payload, self.source.consumer_secret, "ES256", {"kid": parts[2].strip()})
LOGGER.debug("signing payload as secret key", payload=payload, jwt=jwt)
return jwt
@ -55,7 +76,7 @@ class AppleOAuthRedirect(OAuthRedirect):
client_class = AppleOAuthClient
def get_additional_parameters(self, source): # pragma: no cover
def get_additional_parameters(self, source: OAuthSource): # pragma: no cover
return {
"scope": "name email",
"response_mode": "form_post",
@ -74,7 +95,6 @@ class AppleOAuth2Callback(OAuthCallback):
self,
info: dict[str, Any],
) -> dict[str, Any]:
print(info)
return {
"email": info.get("email"),
"name": info.get("name"),
@ -96,3 +116,24 @@ class AppleType(SourceType):
def icon_url(self) -> str:
return "https://appleid.cdn-apple.com/appleid/button/logo"
def login_challenge(self, source: OAuthSource, request: HttpRequest) -> Challenge:
"""Pre-general all the things required for the JS SDK"""
apple_client = AppleOAuthClient(
source,
request,
callback=reverse(
"authentik_sources_oauth:oauth-client-callback",
kwargs={"source_slug": source.slug},
),
)
args = apple_client.get_redirect_args()
return AppleLoginChallenge(
instance={
"client_id": apple_client.get_client_id(),
"scope": "name email",
"redirect_uri": args["redirect_uri"],
"state": args["state"],
"type": ChallengeTypes.NATIVE.value,
}
)

View File

@ -2,9 +2,13 @@
from enum import Enum
from typing import Callable, Optional, Type
from django.http.request import HttpRequest
from django.templatetags.static import static
from django.urls.base import reverse
from structlog.stdlib import get_logger
from authentik.flows.challenge import Challenge, ChallengeTypes, RedirectChallenge
from authentik.sources.oauth.models import OAuthSource
from authentik.sources.oauth.views.callback import OAuthCallback
from authentik.sources.oauth.views.redirect import OAuthRedirect
@ -37,6 +41,19 @@ class SourceType:
"""Get Icon URL for login"""
return static(f"authentik/sources/{self.slug}.svg")
# pylint: disable=unused-argument
def login_challenge(self, source: OAuthSource, request: HttpRequest) -> Challenge:
"""Allow types to return custom challenges"""
return RedirectChallenge(
instance={
"type": ChallengeTypes.REDIRECT.value,
"to": reverse(
"authentik_sources_oauth:oauth-client-login",
kwargs={"source_slug": source.slug},
),
}
)
class SourceTypeManager:
"""Manager to hold all Source types."""

View File

@ -0,0 +1,51 @@
"""Okta OAuth Views"""
from typing import Any
from authentik.sources.oauth.models import OAuthSource
from authentik.sources.oauth.types.azure_ad import AzureADClient
from authentik.sources.oauth.types.manager import MANAGER, SourceType
from authentik.sources.oauth.views.callback import OAuthCallback
from authentik.sources.oauth.views.redirect import OAuthRedirect
class OktaOAuthRedirect(OAuthRedirect):
"""Okta OAuth2 Redirect"""
def get_additional_parameters(self, source: OAuthSource): # pragma: no cover
return {
"scope": "openid email profile",
}
class OktaOAuth2Callback(OAuthCallback):
"""Okta OAuth2 Callback"""
# Okta has the same quirk as azure and throws an error if the access token
# is set via query parameter, so we re-use the azure client
# see https://github.com/goauthentik/authentik/issues/1910
client_class = AzureADClient
def get_user_id(self, info: dict[str, str]) -> str:
return info.get("sub", "")
def get_user_enroll_context(
self,
info: dict[str, Any],
) -> dict[str, Any]:
return {
"username": info.get("nickname"),
"email": info.get("email"),
"name": info.get("name"),
}
@MANAGER.type()
class OktaType(SourceType):
"""Okta Type definition"""
callback_view = OktaOAuth2Callback
redirect_view = OktaOAuthRedirect
name = "Okta"
slug = "okta"
urls_customizable = True

View File

@ -3,6 +3,7 @@ from typing import Optional
from django.contrib.postgres.fields import ArrayField
from django.db import models
from django.http.request import HttpRequest
from django.templatetags.static import static
from django.utils.translation import gettext_lazy as _
from rest_framework.fields import CharField
@ -62,8 +63,7 @@ class PlexSource(Source):
return PlexSourceSerializer
@property
def ui_login_button(self) -> UILoginButton:
def ui_login_button(self, request: HttpRequest) -> UILoginButton:
return UILoginButton(
challenge=PlexAuthenticationChallenge(
{
@ -77,7 +77,6 @@ class PlexSource(Source):
name=self.name,
)
@property
def ui_user_settings(self) -> Optional[UserSettingSerializer]:
return UserSettingSerializer(
data={

View File

@ -167,8 +167,7 @@ class SAMLSource(Source):
reverse(f"authentik_sources_saml:{view}", kwargs={"source_slug": self.slug})
)
@property
def ui_login_button(self) -> UILoginButton:
def ui_login_button(self, request: HttpRequest) -> UILoginButton:
return UILoginButton(
challenge=RedirectChallenge(
instance={

View File

@ -48,7 +48,6 @@ class AuthenticatorDuoStage(ConfigurableStage, Stage):
def component(self) -> str:
return "ak-stage-authenticator-duo-form"
@property
def ui_user_settings(self) -> Optional[UserSettingSerializer]:
return UserSettingSerializer(
data={

View File

@ -141,7 +141,6 @@ class AuthenticatorSMSStage(ConfigurableStage, Stage):
def component(self) -> str:
return "ak-stage-authenticator-sms-form"
@property
def ui_user_settings(self) -> Optional[UserSettingSerializer]:
return UserSettingSerializer(
data={

View File

@ -90,6 +90,5 @@ class AuthenticatorSMSStageTests(APITestCase):
"code": int(self.client.session[SESSION_SMS_DEVICE].token),
},
)
print(response.content)
self.assertEqual(response.status_code, 200)
sms_send_mock.assert_not_called()

View File

@ -31,7 +31,6 @@ class AuthenticatorStaticStage(ConfigurableStage, Stage):
def component(self) -> str:
return "ak-stage-authenticator-static-form"
@property
def ui_user_settings(self) -> Optional[UserSettingSerializer]:
return UserSettingSerializer(
data={

View File

@ -38,7 +38,6 @@ class AuthenticatorTOTPStage(ConfigurableStage, Stage):
def component(self) -> str:
return "ak-stage-authenticator-totp-form"
@property
def ui_user_settings(self) -> Optional[UserSettingSerializer]:
return UserSettingSerializer(
data={

View File

@ -18,7 +18,7 @@ class AuthenticateWebAuthnStageSerializer(StageSerializer):
class Meta:
model = AuthenticateWebAuthnStage
fields = StageSerializer.Meta.fields + ["configure_flow"]
fields = StageSerializer.Meta.fields + ["configure_flow", "user_verification"]
class AuthenticateWebAuthnStageViewSet(UsedByMixin, ModelViewSet):

View File

@ -0,0 +1,25 @@
# Generated by Django 4.0 on 2021-12-14 09:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_stages_authenticator_webauthn", "0004_auto_20210304_1850"),
]
operations = [
migrations.AddField(
model_name="authenticatewebauthnstage",
name="user_verification",
field=models.TextField(
choices=[
("required", "Required"),
("preferred", "Preferred"),
("discouraged", "Discouraged"),
],
default="preferred",
),
),
]

View File

@ -15,9 +15,30 @@ from authentik.core.types import UserSettingSerializer
from authentik.flows.models import ConfigurableStage, Stage
class UserVerification(models.TextChoices):
"""The degree to which the Relying Party wishes to verify a user's identity.
Members:
`REQUIRED`: User verification must occur
`PREFERRED`: User verification would be great, but if not that's okay too
`DISCOURAGED`: User verification should not occur, but it's okay if it does
https://www.w3.org/TR/webauthn-2/#enumdef-userverificationrequirement
"""
REQUIRED = "required"
PREFERRED = "preferred"
DISCOURAGED = "discouraged"
class AuthenticateWebAuthnStage(ConfigurableStage, Stage):
"""WebAuthn stage"""
user_verification = models.TextField(
choices=UserVerification.choices,
default=UserVerification.PREFERRED,
)
@property
def serializer(self) -> BaseSerializer:
from authentik.stages.authenticator_webauthn.api import AuthenticateWebAuthnStageSerializer
@ -34,7 +55,6 @@ class AuthenticateWebAuthnStage(ConfigurableStage, Stage):
def component(self) -> str:
return "ak-stage-authenticator-webauthn-form"
@property
def ui_user_settings(self) -> Optional[UserSettingSerializer]:
return UserSettingSerializer(
data={

View File

@ -14,7 +14,6 @@ from webauthn.helpers.structs import (
PublicKeyCredentialCreationOptions,
RegistrationCredential,
ResidentKeyRequirement,
UserVerificationRequirement,
)
from webauthn.registration.verify_registration_response import VerifiedRegistration
@ -27,7 +26,7 @@ from authentik.flows.challenge import (
)
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
from authentik.flows.stage import ChallengeStageView
from authentik.stages.authenticator_webauthn.models import WebAuthnDevice
from authentik.stages.authenticator_webauthn.models import AuthenticateWebAuthnStage, WebAuthnDevice
from authentik.stages.authenticator_webauthn.utils import get_origin, get_rp_id
LOGGER = get_logger()
@ -83,7 +82,7 @@ class AuthenticatorWebAuthnStageView(ChallengeStageView):
def get_challenge(self, *args, **kwargs) -> Challenge:
# clear session variables prior to starting a new registration
self.request.session.pop("challenge", None)
stage: AuthenticateWebAuthnStage = self.executor.current_stage
user = self.get_pending_user()
registration_options: PublicKeyCredentialCreationOptions = generate_registration_options(
@ -94,10 +93,9 @@ class AuthenticatorWebAuthnStageView(ChallengeStageView):
user_display_name=user.name,
authenticator_selection=AuthenticatorSelectionCriteria(
resident_key=ResidentKeyRequirement.PREFERRED,
user_verification=UserVerificationRequirement.PREFERRED,
user_verification=str(stage.user_verification),
),
)
registration_options.user.id = user.uid
self.request.session["challenge"] = registration_options.challenge
return AuthenticatorWebAuthnChallenge(

View File

@ -29,4 +29,4 @@ class TestEmailStageAPI(APITestCase):
EmailTemplates.ACCOUNT_CONFIRM,
)
with self.assertRaises(ValidationError):
print(EmailStageSerializer().validate_template("foobar"))
EmailStageSerializer().validate_template("foobar")

View File

@ -12,6 +12,7 @@ from django.utils.translation import gettext as _
from drf_spectacular.utils import PolymorphicProxySerializer, extend_schema_field
from rest_framework.fields import BooleanField, CharField, DictField, ListField
from rest_framework.serializers import ValidationError
from sentry_sdk.hub import Hub
from structlog.stdlib import get_logger
from authentik.core.api.utils import PassiveSerializer
@ -25,6 +26,7 @@ from authentik.flows.challenge import (
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
from authentik.flows.stage import PLAN_CONTEXT_PENDING_USER_IDENTIFIER, ChallengeStageView
from authentik.flows.views.executor import SESSION_KEY_APPLICATION_PRE
from authentik.sources.oauth.types.apple import AppleLoginChallenge
from authentik.sources.plex.models import PlexAuthenticationChallenge
from authentik.stages.identification.models import IdentificationStage
from authentik.stages.identification.signals import identification_failed
@ -39,6 +41,7 @@ LOGGER = get_logger()
serializers={
RedirectChallenge().fields["component"].default: RedirectChallenge,
PlexAuthenticationChallenge().fields["component"].default: PlexAuthenticationChallenge,
AppleLoginChallenge().fields["component"].default: AppleLoginChallenge,
},
resource_type_field_name="component",
)
@ -88,8 +91,12 @@ class IdentificationChallengeResponse(ChallengeResponse):
pre_user = self.stage.get_user(uid_field)
if not pre_user:
# Sleep a random time (between 90 and 210ms) to "prevent" user enumeration attacks
sleep(0.30 * SystemRandom().randint(3, 7))
with Hub.current.start_span(
op="authentik.stages.identification.validate_invalid_wait",
description="Sleep random time on invalid user identifier",
):
# Sleep a random time (between 90 and 210ms) to "prevent" user enumeration attacks
sleep(0.030 * SystemRandom().randint(3, 7))
LOGGER.debug("invalid_login", identifier=uid_field)
identification_failed.send(sender=self, request=self.stage.request, uid_field=uid_field)
# We set the pending_user even on failure so it's part of the context, even
@ -112,12 +119,16 @@ class IdentificationChallengeResponse(ChallengeResponse):
if not password:
LOGGER.warning("Password not set for ident+auth attempt")
try:
user = authenticate(
self.stage.request,
current_stage.password_stage.backends,
username=self.pre_user.username,
password=password,
)
with Hub.current.start_span(
op="authentik.stages.identification.authenticate",
description="User authenticate call (combo stage)",
):
user = authenticate(
self.stage.request,
current_stage.password_stage.backends,
username=self.pre_user.username,
password=password,
)
if not user:
raise ValidationError("Failed to authenticate.")
self.pre_user = user
@ -191,7 +202,7 @@ class IdentificationStageView(ChallengeStageView):
current_stage.sources.filter(enabled=True).order_by("name").select_subclasses()
)
for source in sources:
ui_login_button = source.ui_login_button
ui_login_button = source.ui_login_button(self.request)
if ui_login_button:
button = asdict(ui_login_button)
button["challenge"] = ui_login_button.challenge.data

View File

@ -5,8 +5,8 @@ from rest_framework.fields import JSONField
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.groups import GroupMemberSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.users import UserSerializer
from authentik.core.api.utils import is_dict
from authentik.flows.api.stages import StageSerializer
from authentik.stages.invitation.models import Invitation, InvitationStage
@ -46,7 +46,7 @@ class InvitationStageViewSet(UsedByMixin, ModelViewSet):
class InvitationSerializer(ModelSerializer):
"""Invitation Serializer"""
created_by = UserSerializer(read_only=True)
created_by = GroupMemberSerializer(read_only=True)
fixed_data = JSONField(validators=[is_dict], required=False)
class Meta:

View File

@ -63,7 +63,6 @@ class PasswordStage(ConfigurableStage, Stage):
def component(self) -> str:
return "ak-stage-password-form"
@property
def ui_user_settings(self) -> Optional[UserSettingSerializer]:
if not self.configure_flow:
return None

View File

@ -10,6 +10,7 @@ from django.urls import reverse
from django.utils.translation import gettext as _
from rest_framework.exceptions import ErrorDetail, ValidationError
from rest_framework.fields import CharField
from sentry_sdk.hub import Hub
from structlog.stdlib import get_logger
from authentik.core.models import User
@ -43,7 +44,11 @@ def authenticate(request: HttpRequest, backends: list[str], **credentials: Any)
LOGGER.warning("Failed to import backend", path=backend_path)
continue
LOGGER.debug("Attempting authentication...", backend=backend_path)
user = backend.authenticate(request, **credentials)
with Hub.current.start_span(
op="authentik.stages.password.authenticate",
description=backend_path,
):
user = backend.authenticate(request, **credentials)
if user is None:
LOGGER.debug("Backend returned nothing, continuing", backend=backend_path)
continue
@ -120,7 +125,13 @@ class PasswordStageView(ChallengeStageView):
"username": pending_user.username,
}
try:
user = authenticate(self.request, self.executor.current_stage.backends, **auth_kwargs)
with Hub.current.start_span(
op="authentik.stages.password.authenticate",
description="User authenticate call",
):
user = authenticate(
self.request, self.executor.current_stage.backends, **auth_kwargs
)
except PermissionDenied:
del auth_kwargs["password"]
# User was found, but permission was denied (i.e. user is not active)

View File

@ -4,6 +4,7 @@ from typing import Any
from django.db.models import F, Q
from django.db.models import Value as V
from django.http.request import HttpRequest
from sentry_sdk.hub import Hub
from authentik.lib.config import CONFIG
from authentik.tenants.models import Tenant
@ -28,7 +29,12 @@ def get_tenant_for_request(request: HttpRequest) -> Tenant:
def context_processor(request: HttpRequest) -> dict[str, Any]:
"""Context Processor that injects tenant object into every template"""
tenant = getattr(request, "tenant", DEFAULT_TENANT)
trace = ""
span = Hub.current.scope.span
if span:
trace = span.to_traceparent()
return {
"tenant": tenant,
"footer_links": CONFIG.y("footer_links"),
"sentry_trace": trace,
}

View File

@ -17,7 +17,7 @@ services:
image: redis:alpine
restart: unless-stopped
server:
image: ${AUTHENTIK_IMAGE:-goauthentik.io/server}:${AUTHENTIK_TAG:-2021.12.1-rc4}
image: ${AUTHENTIK_IMAGE:-goauthentik.io/server}:${AUTHENTIK_TAG:-2021.12.1-rc5}
restart: unless-stopped
command: server
environment:
@ -38,7 +38,7 @@ services:
- "0.0.0.0:9000:9000"
- "0.0.0.0:9443:9443"
worker:
image: ${AUTHENTIK_IMAGE:-goauthentik.io/server}:${AUTHENTIK_TAG:-2021.12.1-rc4}
image: ${AUTHENTIK_IMAGE:-goauthentik.io/server}:${AUTHENTIK_TAG:-2021.12.1-rc5}
restart: unless-stopped
command: worker
environment:

2
go.mod
View File

@ -28,7 +28,7 @@ require (
github.com/pquerna/cachecontrol v0.0.0-20201205024021-ac21108117ac // indirect
github.com/prometheus/client_golang v1.11.0
github.com/sirupsen/logrus v1.8.1
goauthentik.io/api v0.2021104.11
goauthentik.io/api v0.2021104.17
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 // indirect
golang.org/x/net v0.0.0-20210510120150-4163338589ed // indirect
golang.org/x/oauth2 v0.0.0-20210323180902-22b0adad7558

4
go.sum
View File

@ -558,8 +558,8 @@ go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
goauthentik.io/api v0.2021104.11 h1:LqT0LM0e/RRrxPuo6Xl5uz3PCR5ytuE+YlNlfW9w0yU=
goauthentik.io/api v0.2021104.11/go.mod h1:02nnD4FRd8lu8A1+ZuzqownBgvAhdCKzqkKX8v7JMTE=
goauthentik.io/api v0.2021104.17 h1:NnfdoIlAekwPu+G7h7X/SGbWjWSypEy/pGQDD7/J+Vw=
goauthentik.io/api v0.2021104.17/go.mod h1:02nnD4FRd8lu8A1+ZuzqownBgvAhdCKzqkKX8v7JMTE=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=

View File

@ -17,4 +17,4 @@ func OutpostUserAgent() string {
return fmt.Sprintf("authentik-outpost@%s (build=%s)", VERSION, BUILD())
}
const VERSION = "2021.12.1-rc4"
const VERSION = "2021.12.1-rc5"

View File

@ -47,7 +47,7 @@ type APIController struct {
// NewAPIController initialise new API Controller instance from URL and API token
func NewAPIController(akURL url.URL, token string) *APIController {
rsp := sentry.StartSpan(context.TODO(), "authentik.outposts.init")
rsp := sentry.StartSpan(context.Background(), "authentik.outposts.init")
config := api.NewConfiguration()
config.Host = akURL.Host

View File

@ -107,6 +107,7 @@ func (ac *APIController) reconnectWS() {
}
} else {
ac.wsIsReconnecting = false
ac.wsBackoffMultiplier = 1
return
}
}

View File

@ -2,6 +2,7 @@ package ak
import (
"context"
"fmt"
"net/http"
"github.com/getsentry/sentry-go"
@ -19,6 +20,8 @@ func NewTracingTransport(ctx context.Context, inner http.RoundTripper) *tracingT
func (tt *tracingTransport) RoundTrip(r *http.Request) (*http.Response, error) {
span := sentry.StartSpan(tt.ctx, "authentik.go.http_request")
r.Header.Set("sentry-trace", span.ToSentryTrace())
span.Description = fmt.Sprintf("%s %s", r.Method, r.URL.String())
span.SetTag("url", r.URL.String())
span.SetTag("method", r.Method)
defer span.Finish()

View File

@ -168,6 +168,7 @@ func NewApplication(p api.ProxyOutpostConfig, c *http.Client, cs *ak.CryptoStore
func (a *Application) IsAllowlisted(r *http.Request) bool {
for _, u := range a.UnauthenticatedRegex {
a.log.WithField("regex", u.String()).WithField("url", r.URL.Path).Trace("Matching URL against allow list")
if u.MatchString(r.URL.Path) {
return true
}

View File

@ -2,6 +2,7 @@ package application
import (
"fmt"
"math"
"os"
"strconv"
@ -18,6 +19,7 @@ func (a *Application) getStore(p api.ProxyOutpostConfig) sessions.Store {
if err != nil {
panic(err)
}
rs.SetMaxLength(math.MaxInt64)
if p.TokenValidity.IsSet() {
t := p.TokenValidity.Get()
// Add one to the validity to ensure we don't have a session with indefinite length
@ -27,14 +29,22 @@ func (a *Application) getStore(p api.ProxyOutpostConfig) sessions.Store {
a.log.Info("using redis session backend")
store = rs
} else {
cs := sessions.NewFilesystemStore(os.TempDir(), []byte(*p.CookieSecret))
dir := os.TempDir()
cs := sessions.NewFilesystemStore(dir, []byte(*p.CookieSecret))
cs.Options.Domain = *p.CookieDomain
// https://github.com/markbates/goth/commit/7276be0fdf719ddff753f3574ef0f967e4a5a5f7
// set the maxLength of the cookies stored on the disk to a larger number to prevent issues with:
// securecookie: the value is too long
// when using OpenID Connect , since this can contain a large amount of extra information in the id_token
// Note, when using the FilesystemStore only the session.ID is written to a browser cookie, so this is explicit for the storage on disk
cs.MaxLength(math.MaxInt64)
if p.TokenValidity.IsSet() {
t := p.TokenValidity.Get()
// Add one to the validity to ensure we don't have a session with indefinite length
cs.Options.MaxAge = int(*t) + 1
}
a.log.Info("using filesystem session backend")
a.log.WithField("dir", dir).Info("using filesystem session backend")
store = cs
}
return store

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
openapi: 3.0.3
info:
title: authentik
version: 2021.12.1-rc4
version: 2021.12.1-rc5
description: Making authentication simple.
contact:
email: hello@beryju.org
@ -3909,6 +3909,36 @@ paths:
$ref: '#/components/schemas/ValidationError'
'403':
$ref: '#/components/schemas/GenericError'
/events/events/per_month/:
get:
operationId: events_events_per_month_list
description: Get the count of events per month
parameters:
- in: query
name: action
schema:
type: string
- in: query
name: query
schema:
type: string
tags:
- events
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Coordinate'
description: ''
'400':
$ref: '#/components/schemas/ValidationError'
'403':
$ref: '#/components/schemas/GenericError'
/events/events/top_per_user/:
get:
operationId: events_events_top_per_user_list
@ -3918,56 +3948,10 @@ paths:
name: action
schema:
type: string
- in: query
name: client_ip
schema:
type: string
- in: query
name: context_authorized_app
schema:
type: string
description: Context Authorized application
- in: query
name: context_model_app
schema:
type: string
description: Context Model App
- in: query
name: context_model_name
schema:
type: string
description: Context Model Name
- in: query
name: context_model_pk
schema:
type: string
description: Context Model Primary Key
- name: ordering
required: false
in: query
description: Which field to use when ordering the results.
schema:
type: string
- name: search
required: false
in: query
description: A search term.
schema:
type: string
- in: query
name: tenant_name
schema:
type: string
description: Tenant name
- in: query
name: top_n
schema:
type: integer
- in: query
name: username
schema:
type: string
description: Username
tags:
- events
security:
@ -7539,6 +7523,7 @@ paths:
- configuration_error
- custom_
- email_sent
- flow_execution
- impersonation_ended
- impersonation_started
- invitation_used
@ -15395,6 +15380,14 @@ paths:
schema:
type: string
format: uuid
- in: query
name: user_verification
schema:
type: string
enum:
- discouraged
- preferred
- required
tags:
- stages
security:
@ -19086,6 +19079,46 @@ components:
- authentik.managed
- authentik.core
type: string
AppleChallengeResponseRequest:
type: object
description: Pseudo class for plex response
properties:
component:
type: string
minLength: 1
default: ak-flow-sources-oauth-apple
AppleLoginChallenge:
type: object
description: Special challenge for apple-native authentication flow, which happens
on the client.
properties:
type:
$ref: '#/components/schemas/ChallengeChoices'
flow_info:
$ref: '#/components/schemas/ContextualFlowInfo'
component:
type: string
default: ak-flow-sources-oauth-apple
response_errors:
type: object
additionalProperties:
type: array
items:
$ref: '#/components/schemas/ErrorDetail'
client_id:
type: string
scope:
type: string
redirect_uri:
type: string
state:
type: string
required:
- client_id
- redirect_uri
- scope
- state
- type
Application:
type: object
description: Application Serializer
@ -19200,6 +19233,8 @@ components:
nullable: true
description: Flow used by an authenticated user to configure this Stage.
If empty, user will not be able to configure this stage.
user_verification:
$ref: '#/components/schemas/UserVerificationEnum'
required:
- component
- meta_model_name
@ -19224,6 +19259,8 @@ components:
nullable: true
description: Flow used by an authenticated user to configure this Stage.
If empty, user will not be able to configure this stage.
user_verification:
$ref: '#/components/schemas/UserVerificationEnum'
required:
- name
AuthenticatedSession:
@ -20225,6 +20262,7 @@ components:
ChallengeTypes:
oneOf:
- $ref: '#/components/schemas/AccessDeniedChallenge'
- $ref: '#/components/schemas/AppleLoginChallenge'
- $ref: '#/components/schemas/AuthenticatorDuoChallenge'
- $ref: '#/components/schemas/AuthenticatorSMSChallenge'
- $ref: '#/components/schemas/AuthenticatorStaticChallenge'
@ -20246,6 +20284,7 @@ components:
propertyName: component
mapping:
ak-stage-access-denied: '#/components/schemas/AccessDeniedChallenge'
ak-flow-sources-oauth-apple: '#/components/schemas/AppleLoginChallenge'
ak-stage-authenticator-duo: '#/components/schemas/AuthenticatorDuoChallenge'
ak-stage-authenticator-sms: '#/components/schemas/AuthenticatorSMSChallenge'
ak-stage-authenticator-static: '#/components/schemas/AuthenticatorStaticChallenge'
@ -21080,6 +21119,7 @@ components:
- source_linked
- impersonation_started
- impersonation_ended
- flow_execution
- policy_execution
- policy_exception
- property_mapping_exception
@ -21387,6 +21427,7 @@ components:
- title
FlowChallengeResponseRequest:
oneOf:
- $ref: '#/components/schemas/AppleChallengeResponseRequest'
- $ref: '#/components/schemas/AuthenticatorDuoChallengeResponseRequest'
- $ref: '#/components/schemas/AuthenticatorSMSChallengeResponseRequest'
- $ref: '#/components/schemas/AuthenticatorStaticChallengeResponseRequest'
@ -21405,6 +21446,7 @@ components:
discriminator:
propertyName: component
mapping:
ak-flow-sources-oauth-apple: '#/components/schemas/AppleChallengeResponseRequest'
ak-stage-authenticator-duo: '#/components/schemas/AuthenticatorDuoChallengeResponseRequest'
ak-stage-authenticator-sms: '#/components/schemas/AuthenticatorSMSChallengeResponseRequest'
ak-stage-authenticator-static: '#/components/schemas/AuthenticatorStaticChallengeResponseRequest'
@ -22071,7 +22113,7 @@ components:
additionalProperties: {}
created_by:
allOf:
- $ref: '#/components/schemas/User'
- $ref: '#/components/schemas/GroupMember'
readOnly: true
single_use:
type: boolean
@ -22711,11 +22753,13 @@ components:
oneOf:
- $ref: '#/components/schemas/RedirectChallenge'
- $ref: '#/components/schemas/PlexAuthenticationChallenge'
- $ref: '#/components/schemas/AppleLoginChallenge'
discriminator:
propertyName: component
mapping:
xak-flow-redirect: '#/components/schemas/RedirectChallenge'
ak-flow-sources-plex: '#/components/schemas/PlexAuthenticationChallenge'
ak-flow-sources-oauth-apple: '#/components/schemas/AppleLoginChallenge'
LoginMetrics:
type: object
description: Login Metrics per 1h
@ -26565,6 +26609,8 @@ components:
nullable: true
description: Flow used by an authenticated user to configure this Stage.
If empty, user will not be able to configure this stage.
user_verification:
$ref: '#/components/schemas/UserVerificationEnum'
PatchedAuthenticatorDuoStageRequest:
type: object
description: AuthenticatorDuoStage Serializer
@ -28992,6 +29038,7 @@ components:
- github
- google
- openidconnect
- okta
- reddit
- twitter
type: string
@ -31188,6 +31235,12 @@ components:
- pk
- source
- user
UserVerificationEnum:
enum:
- required
- preferred
- discouraged
type: string
UserWriteStage:
type: object
description: UserWriteStage Serializer

View File

@ -40,7 +40,7 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
sleep(1)
client: DockerClient = from_env()
container = client.containers.run(
image="beryju.org/oidc-test-client:latest",
image="ghcr.io/beryju/oidc-test-client:latest",
detach=True,
network_mode="host",
auto_remove=True,

View File

@ -40,7 +40,7 @@ class TestProviderOAuth2OIDCImplicit(SeleniumTestCase):
sleep(1)
client: DockerClient = from_env()
container = client.containers.run(
image="beryju.org/oidc-test-client:latest",
image="ghcr.io/beryju/oidc-test-client:latest",
detach=True,
network_mode="host",
auto_remove=True,
@ -145,7 +145,6 @@ class TestProviderOAuth2OIDCImplicit(SeleniumTestCase):
self.wait.until(ec.presence_of_element_located((By.CSS_SELECTOR, "pre")))
sleep(1)
body = loads(self.driver.find_element(By.CSS_SELECTOR, "pre").text)
print(body)
self.assertEqual(body["profile"]["nickname"], self.user.username)
self.assertEqual(body["profile"]["name"], self.user.name)
self.assertEqual(body["profile"]["email"], self.user.email)

View File

@ -39,7 +39,7 @@ class TestProviderSAML(SeleniumTestCase):
if force_post:
metadata_url += f"&force_binding={SAML_BINDING_POST}"
container = client.containers.run(
image="beryju.org/saml-test-sp:latest",
image="ghcr.io/beryju/saml-test-sp:latest",
detach=True,
network_mode="host",
auto_remove=True,

View File

@ -229,7 +229,7 @@ class TestSourceOAuth1(SeleniumTestCase):
def get_container_specs(self) -> Optional[dict[str, Any]]:
return {
"image": "beryju.org/oauth1-test-server:latest",
"image": "ghcr.io/beryju/oauth1-test-server:latest",
"detach": True,
"network_mode": "host",
"auto_remove": True,

View File

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 400 134.7" style="enable-background:new 0 0 400 134.7;" xml:space="preserve">
<style type="text/css">
.st0{fill:#007DC1;}
</style>
<g>
<g>
<g>
<path class="st0" d="M50.3,33.8C22.5,33.8,0,56.3,0,84.1s22.5,50.3,50.3,50.3s50.3-22.5,50.3-50.3S78.1,33.8,50.3,33.8z
M50.3,109.3c-13.9,0-25.2-11.3-25.2-25.2s11.3-25.2,25.2-25.2s25.2,11.3,25.2,25.2S64.2,109.3,50.3,109.3z"/>
</g>
<path class="st0" d="M138.7,101c0-4,4.8-5.9,7.6-3.1c12.6,12.8,33.4,34.8,33.5,34.9c0.3,0.3,0.6,0.8,1.8,1.2
c0.5,0.2,1.3,0.2,2.2,0.2l22.7,0c4.1,0,5.3-4.7,3.4-7.1l-37.6-38.5l-2-2c-4.3-5.1-3.8-7.1,1.1-12.3L201.2,41c1.9-2.4,0.7-7-3.5-7
h-20.6c-0.8,0-1.4,0-2,0.2c-1.2,0.4-1.7,0.8-2,1.2c-0.1,0.1-16.6,17.9-26.8,28.8c-2.8,3-7.8,1-7.8-3.1l0-57.1c0-2.9-2.4-4-4.3-4
h-16.8c-2.9,0-4.3,1.9-4.3,3.6v126.6c0,2.9,2.4,3.7,4.4,3.7h16.8c2.6,0,4.3-1.9,4.3-3.8v-1.3V101z"/>
<path class="st0" d="M275.9,129.6l-1.8-16.8c-0.2-2.3-2.4-3.9-4.7-3.5c-1.3,0.2-2.6,0.3-3.9,0.3c-13.4,0-24.3-10.5-25.1-23.8
c0-0.4,0-0.9,0-1.4V63.8c0-2.7,2-4.9,4.7-4.9l22.5,0c1.6,0,4-1.4,4-4.3V38.7c0-3.1-2-4.7-3.8-4.7h-22.7c-2.6,0-4.7-1.9-4.8-4.5
l0-25.5c0-1.6-1.2-4-4.3-4h-16.7c-2.1,0-4.1,1.3-4.1,3.9c0,0,0,81.5,0,81.9c0.7,27.2,23,48.9,50.3,48.9c2.3,0,4.5-0.2,6.7-0.5
C274.6,133.9,276.2,131.9,275.9,129.6z"/>
</g>
<g>
<path class="st0" d="M397.1,108.5c-14.2,0-16.4-5.1-16.4-24.2c0-0.1,0-0.1,0-0.2l0-45.9c0-1.6-1.2-4.3-4.4-4.3h-16.8
c-2.1,0-4.4,1.7-4.4,4.3l0,2.1c-7.3-4.2-15.8-6.6-24.8-6.6c-27.8,0-50.3,22.5-50.3,50.3c0,27.8,22.5,50.3,50.3,50.3
c12.5,0,23.9-4.6,32.7-12.1c4.7,7.2,12.3,12,24.2,12.1c2,0,12.8,0.4,12.8-4.7v-17.9C400,110.2,398.8,108.5,397.1,108.5z
M330.4,109.3c-13.9,0-25.2-11.3-25.2-25.2c0-13.9,11.3-25.2,25.2-25.2c13.9,0,25.2,11.3,25.2,25.2
C355.5,98,344.2,109.3,330.4,109.3z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

1545
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -45,24 +45,24 @@
]
},
"dependencies": {
"@babel/core": "^7.16.0",
"@babel/plugin-proposal-decorators": "^7.16.4",
"@babel/plugin-transform-runtime": "^7.16.4",
"@babel/preset-env": "^7.16.4",
"@babel/preset-typescript": "^7.16.0",
"@babel/core": "^7.16.5",
"@babel/plugin-proposal-decorators": "^7.16.5",
"@babel/plugin-transform-runtime": "^7.16.5",
"@babel/preset-env": "^7.16.5",
"@babel/preset-typescript": "^7.16.5",
"@fortawesome/fontawesome-free": "^5.15.4",
"@goauthentik/api": "^2021.10.4-1639076050",
"@goauthentik/api": "^2021.10.4-1639516687",
"@jackfranklin/rollup-plugin-markdown": "^0.3.0",
"@lingui/cli": "^3.13.0",
"@lingui/core": "^3.13.0",
"@lingui/detect-locale": "^3.13.0",
"@lingui/macro": "^3.13.0",
"@patternfly/patternfly": "^4.159.1",
"@patternfly/patternfly": "^4.164.2",
"@polymer/iron-form": "^3.0.1",
"@polymer/paper-input": "^3.2.1",
"@rollup/plugin-babel": "^5.3.0",
"@rollup/plugin-commonjs": "^21.0.1",
"@rollup/plugin-node-resolve": "^13.0.6",
"@rollup/plugin-node-resolve": "^13.1.1",
"@rollup/plugin-replace": "^3.0.0",
"@rollup/plugin-typescript": "^8.3.0",
"@sentry/browser": "^6.16.1",
@ -72,8 +72,8 @@
"@types/chart.js": "^2.9.34",
"@types/codemirror": "5.60.5",
"@types/grecaptcha": "^3.0.3",
"@typescript-eslint/eslint-plugin": "^5.6.0",
"@typescript-eslint/parser": "^5.6.0",
"@typescript-eslint/eslint-plugin": "^5.7.0",
"@typescript-eslint/parser": "^5.7.0",
"@webcomponents/webcomponentsjs": "^2.6.0",
"babel-plugin-macros": "^3.1.0",
"base64-js": "^1.5.1",
@ -99,7 +99,7 @@
"rollup-plugin-terser": "^7.0.2",
"ts-lit-plugin": "^1.2.1",
"tslib": "^2.3.1",
"typescript": "^4.5.3",
"typescript": "^4.5.4",
"webcomponent-qr-code": "^1.0.5",
"yaml": "^1.10.2"
}

View File

@ -3,6 +3,7 @@ import { getCookie } from "../utils";
import { APIMiddleware } from "../elements/notifications/APIDrawer";
import { MessageMiddleware } from "../elements/messages/Middleware";
import { VERSION } from "../constants";
import { getMetaContent } from "@sentry/tracing/dist/browser/browsertracing";
export class LoggingMiddleware implements Middleware {
@ -53,6 +54,7 @@ export const DEFAULT_CONFIG = new Configuration({
basePath: process.env.AK_API_BASE_PATH + "/api/v3",
headers: {
"X-CSRFToken": getCookie("authentik_csrf"),
"sentry-trace": getMetaContent("sentry-trace") || "",
},
middleware: [
new APIMiddleware(),

View File

@ -27,7 +27,10 @@ export function configureSentry(canDoPpi: boolean = false): Promise<Config> {
],
tracesSampleRate: config.errorReporting.tracesSampleRate,
environment: config.errorReporting.environment,
beforeSend: async (event: Sentry.Event, hint: Sentry.EventHint): Promise<Sentry.Event | null> => {
beforeSend: async (event: Sentry.Event, hint: Sentry.EventHint | undefined): Promise<Sentry.Event | null> => {
if (!hint) {
return event;
}
if (hint.originalException instanceof SentryIgnoredError) {
return null;
}
@ -40,8 +43,13 @@ export function configureSentry(canDoPpi: boolean = false): Promise<Config> {
Sentry.setTag(TAG_SENTRY_CAPABILITIES, config.capabilities.join(","));
if (window.location.pathname.includes("if/")) {
// Get the interface name from URL
const intf = window.location.pathname.replace(/.+if\/(.+)\//, "$1");
Sentry.setTag(TAG_SENTRY_COMPONENT, `web/${intf}`);
const pathMatches = window.location.pathname.match(/.+if\/(\w+)\//);
let currentInterface = "unknown";
if (pathMatches && pathMatches.length >= 2) {
currentInterface = pathMatches[1];
}
Sentry.setTag(TAG_SENTRY_COMPONENT, `web/${currentInterface}`);
Sentry.configureScope((scope) => scope.setTransactionName(`authentik.web.if.${currentInterface}`));
}
if (config.errorReporting.sendPii && canDoPpi) {
me().then(user => {

View File

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

View File

@ -108,10 +108,11 @@ export class PageHeader extends LitElement {
renderIcon(): TemplateResult {
if (this.icon) {
if (this.iconImage) {
if (this.iconImage && !this.icon.startsWith("fa://")) {
return html`<img class="pf-icon" src="${this.icon}" />&nbsp;`;
}
return html`<i class=${this.icon}></i>&nbsp;`;
const icon = this.icon.replaceAll("fa://", "fa ");
return html`<i class=${icon}></i>&nbsp;`;
}
return html``;
}
@ -132,7 +133,10 @@ export class PageHeader extends LitElement {
</button>
<section class="pf-c-page__main-section pf-m-light">
<div class="pf-c-content">
<h1>${this.renderIcon()} ${this.header}</h1>
<h1>
${this.renderIcon()}
<slot name="header"> ${this.header} </slot>
</h1>
${this.description ? html`<p>${this.description}</p>` : html``}
</div>
</section>

View File

@ -18,6 +18,9 @@ export class AggregateCard extends LitElement {
@property()
headerLink?: string;
@property({ type: Boolean })
isCenter = true;
static get styles(): CSSResult[] {
return [PFBase, PFCard, PFFlex, AKGlobal].concat([
css`
@ -59,7 +62,9 @@ export class AggregateCard extends LitElement {
</div>
${this.renderHeaderLink()}
</div>
<div class="pf-c-card__body center-value">${this.renderInner()}</div>
<div class="pf-c-card__body ${this.isCenter ? "center-value" : ""}">
${this.renderInner()}
</div>
</div>`;
}
}

View File

@ -0,0 +1,52 @@
import { ChartData, Tick } from "chart.js";
import { t } from "@lingui/macro";
import { customElement, property } from "lit/decorators.js";
import { Coordinate, EventActions, EventsApi } from "@goauthentik/api";
import { DEFAULT_CONFIG } from "../../api/Config";
import { AKChart } from "./Chart";
@customElement("ak-charts-admin-model-per-day")
export class AdminModelPerDay extends AKChart<Coordinate[]> {
@property()
action: EventActions = EventActions.ModelCreated;
@property({ attribute: false })
query?: { [key: string]: unknown } | undefined;
apiRequest(): Promise<Coordinate[]> {
return new EventsApi(DEFAULT_CONFIG).eventsEventsPerMonthList({
action: this.action,
query: JSON.stringify(this.query || {}),
});
}
timeTickCallback(tickValue: string | number, index: number, ticks: Tick[]): string {
const valueStamp = ticks[index];
const delta = Date.now() - valueStamp.value;
const ago = Math.round(delta / 1000 / 3600 / 24);
return t`${ago} days ago`;
}
getChartData(data: Coordinate[]): ChartData {
return {
datasets: [
{
label: t`Objects created`,
backgroundColor: "rgba(189, 229, 184, .5)",
spanGaps: true,
data:
data.map((cord) => {
return {
x: cord.xCord || 0,
y: cord.yCord || 0,
};
}) || [],
},
],
};
}
}

View File

@ -5,6 +5,8 @@ import { ArcElement, BarElement } from "chart.js";
import { LinearScale, TimeScale } from "chart.js";
import "chartjs-adapter-moment";
import { t } from "@lingui/macro";
import { CSSResult, LitElement, TemplateResult, css, html } from "lit";
import { property } from "lit/decorators.js";
@ -114,6 +116,13 @@ export abstract class AKChart<T> extends LitElement {
];
}
timeTickCallback(tickValue: string | number, index: number, ticks: Tick[]): string {
const valueStamp = ticks[index];
const delta = Date.now() - valueStamp.value;
const ago = Math.round(delta / 1000 / 3600);
return t`${ago} hours ago`;
}
getOptions(): ChartOptions {
return {
maintainAspectRatio: false,
@ -122,15 +131,8 @@ export abstract class AKChart<T> extends LitElement {
type: "time",
display: true,
ticks: {
callback: function (
tickValue: string | number,
index: number,
ticks: Tick[],
): string {
const valueStamp = ticks[index];
const delta = Date.now() - valueStamp.value;
const ago = Math.round(delta / 1000 / 3600);
return `${ago} Hours ago`;
callback: (tickValue: string | number, index: number, ticks: Tick[]) => {
return this.timeTickCallback(tickValue, index, ticks);
},
autoSkip: true,
maxTicksLimit: 8,

View File

@ -187,6 +187,7 @@ export class DeleteBulkForm extends ModalButton {
<p class="pf-c-title">
${t`Are you sure you want to delete ${this.objects.length} ${this.objectLabel}?`}
</p>
<slot name="notice"></slot>
</form>
</section>
<section class="pf-c-page__main-section">

View File

@ -102,6 +102,7 @@ export class APIDrawer extends LitElement {
<div class="pf-c-notification-drawer__header">
<div class="text">
<h1 class="pf-c-notification-drawer__header-title">${t`API Requests`}</h1>
<a href="/api/v3/" target="_blank">${t`Open API Browser`}</a>
</div>
<div class="pf-c-notification-drawer__header-action">
<div class="pf-c-notification-drawer__header-action-close">

View File

@ -30,6 +30,19 @@ window.addEventListener("load", () => {
})();
});
export function paramURL(url: string, params?: { [key: string]: unknown }): string {
let finalUrl = "#";
finalUrl += url;
if (params) {
finalUrl += ";";
finalUrl += encodeURIComponent(JSON.stringify(params));
}
return finalUrl;
}
export function navigate(url: string, params?: { [key: string]: unknown }): void {
window.location.assign(paramURL(url, params));
}
@customElement("ak-router-outlet")
export class RouterOutlet extends LitElement {
@property({ attribute: false })

View File

@ -32,6 +32,7 @@ import "../elements/LoadingOverlay";
import { first } from "../utils";
import "./FlowInspector";
import "./access_denied/FlowAccessDenied";
import "./sources/apple/AppleLoginInit";
import "./sources/plex/PlexLoginInit";
import "./stages/RedirectStage";
import "./stages/authenticator_duo/AuthenticatorDuoStage";
@ -321,6 +322,11 @@ export class FlowExecutor extends LitElement implements StageHost {
.host=${this as StageHost}
.challenge=${this.challenge}
></ak-flow-sources-plex>`;
case "ak-flow-sources-oauth-apple":
return html`<ak-flow-sources-oauth-apple
.host=${this as StageHost}
.challenge=${this.challenge}
></ak-flow-sources-oauth-apple>`;
default:
break;
}

View File

@ -0,0 +1,79 @@
import { t } from "@lingui/macro";
import { CSSResult, TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import AKGlobal from "../../../authentik.css";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
import PFLogin from "@patternfly/patternfly/components/Login/login.css";
import PFTitle from "@patternfly/patternfly/components/Title/title.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { AppleChallengeResponseRequest, AppleLoginChallenge } from "@goauthentik/api";
import "../../../elements/EmptyState";
import { BaseStage } from "../../stages/base";
@customElement("ak-flow-sources-oauth-apple")
export class AppleLoginInit extends BaseStage<AppleLoginChallenge, AppleChallengeResponseRequest> {
@property({ type: Boolean })
isModalShown = false;
static get styles(): CSSResult[] {
return [PFBase, PFLogin, PFForm, PFFormControl, PFButton, PFTitle, AKGlobal];
}
firstUpdated(): void {
const appleAuth = document.createElement("script");
appleAuth.src =
"https://appleid.cdn-apple.com/appleauth/static/jsapi/appleid/1/en_US/appleid.auth.js";
appleAuth.type = "text/javascript";
appleAuth.onload = () => {
AppleID.auth.init({
clientId: this.challenge?.clientId,
scope: this.challenge.scope,
redirectURI: this.challenge.redirectUri,
state: this.challenge.state,
usePopup: false,
});
AppleID.auth.signIn();
this.isModalShown = true;
};
document.head.append(appleAuth);
//Listen for authorization success
document.addEventListener("AppleIDSignInOnSuccess", () => {
//handle successful response
});
//Listen for authorization failures
document.addEventListener("AppleIDSignInOnFailure", (error) => {
console.warn(error);
this.isModalShown = false;
});
}
render(): TemplateResult {
return html`<header class="pf-c-login__main-header">
<h1 class="pf-c-title pf-m-3xl">${t`Authenticating with Apple...`}</h1>
</header>
<div class="pf-c-login__main-body">
<form class="pf-c-form">
<ak-empty-state ?loading="${true}"> </ak-empty-state>
${!this.isModalShown
? html`<button
class="pf-c-button pf-m-primary pf-m-block"
@click=${() => {
AppleID.auth.signIn();
}}
>
${t`Retry`}
</button>`
: html``}
</form>
</div>
<footer class="pf-c-login__main-footer">
<ul class="pf-c-login__main-footer-links"></ul>
</footer>`;
}
}

14
web/src/flows/sources/apple/apple.d.ts vendored Normal file
View File

@ -0,0 +1,14 @@
declare namespace AppleID {
const auth: AppleIDAuth;
class AppleIDAuth {
init({
clientId: string,
scope: string,
redirectURI: string,
state: string,
usePopup: boolean,
}): void;
async signIn(): Promise<void>;
}
}

View File

@ -51,6 +51,7 @@ export class WebAuthnAuthenticatorRegisterStage extends BaseStage<
// byte arrays as expected by the spec.
const publicKeyCredentialCreateOptions = transformCredentialCreateOptions(
this.challenge?.registration as PublicKeyCredentialCreationOptions,
this.challenge?.registration.user.id,
);
// request the authenticator(s) to create a new credential keypair.

View File

@ -8,15 +8,26 @@ export function b64RawEnc(buf: Uint8Array): string {
return base64js.fromByteArray(buf).replace(/\+/g, "-").replace(/\//g, "_");
}
export function u8arr(input: string): Uint8Array {
return Uint8Array.from(atob(input.replace(/_/g, "/").replace(/-/g, "+")), (c) =>
c.charCodeAt(0),
);
}
/**
* Transforms items in the credentialCreateOptions generated on the server
* into byte arrays expected by the navigator.credentials.create() call
*/
export function transformCredentialCreateOptions(
credentialCreateOptions: PublicKeyCredentialCreationOptions,
userId: string,
): PublicKeyCredentialCreationOptions {
const user = credentialCreateOptions.user;
user.id = u8arr(b64enc(credentialCreateOptions.user.id as Uint8Array));
// 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(escape(window.atob(userId)));
user.id = u8arr(b64enc(u8arr(stringId)));
const challenge = u8arr(credentialCreateOptions.challenge.toString());
const transformedCredentialCreateOptions = Object.assign({}, credentialCreateOptions, {
@ -63,12 +74,6 @@ export function transformNewAssertionForServer(newAssertion: PublicKeyCredential
};
}
function u8arr(input: string): Uint8Array {
return Uint8Array.from(atob(input.replace(/_/g, "/").replace(/-/g, "+")), (c) =>
c.charCodeAt(0),
);
}
export function transformCredentialRequestOptions(
credentialRequestOptions: PublicKeyCredentialRequestOptions,
): PublicKeyCredentialRequestOptions {

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