Compare commits
113 Commits
version/20
...
version/20
Author | SHA1 | Date | |
---|---|---|---|
897f6f3473 | |||
b70b44490b | |||
77a5a58cb9 | |||
f3b227434e | |||
2ae164df78 | |||
9b09793230 | |||
f8a401aeca | |||
ffbab2cd68 | |||
734e5fcab4 | |||
78578c6c9d | |||
0ccec96490 | |||
8022d0801d | |||
d79975c409 | |||
20d65035d5 | |||
8d6227377f | |||
4bc50e7f57 | |||
945e42c940 | |||
052bb28086 | |||
4a84b7e2d5 | |||
4d27694706 | |||
16cfa8cae2 | |||
1a20c8ffc1 | |||
d7ad5f6a16 | |||
5af9a3d3be | |||
dec34bc948 | |||
cff37caa57 | |||
cc6d5765f2 | |||
2ec1ff2ebb | |||
884c2bd0e9 | |||
2c938ec9dc | |||
9733caf3b7 | |||
494af0a430 | |||
10e50bc77f | |||
44bfbb9e49 | |||
5be152e12d | |||
b0efab6d6d | |||
f2725b88c8 | |||
24cc123029 | |||
d75c9997f6 | |||
0a20a30af3 | |||
c60ba91fee | |||
37927c9361 | |||
0a63441935 | |||
6b7a8b6ac7 | |||
cba255eaaa | |||
859cf2bd8f | |||
a2578ffaad | |||
888526a2a7 | |||
0d00b9cc0d | |||
27cc5d7138 | |||
b2f077645a | |||
2878597603 | |||
5face5410f | |||
1b8750e13b | |||
e27a6fdeeb | |||
a9af40f85c | |||
59f04963be | |||
033c9a3bd3 | |||
09e3d616e9 | |||
0b280c0a47 | |||
07a4f474f4 | |||
244dc671db | |||
4308136108 | |||
69a0153619 | |||
2655768f5a | |||
73c55b56a0 | |||
bcbdd6c26f | |||
00e9b91f56 | |||
4cf76fdcda | |||
c4832206fa | |||
d05562a388 | |||
f217d34a98 | |||
89f2967f69 | |||
9a6a3e66b8 | |||
2f4b18ebbd | |||
20572c728d | |||
aad753de68 | |||
a79a150a1f | |||
8b23e4701a | |||
a366d61891 | |||
9a13dfd63a | |||
32d80829e2 | |||
f6953296d8 | |||
e4790f9060 | |||
58712047e1 | |||
85915905dc | |||
52f2838f57 | |||
12e2f7b945 | |||
45d47f828a | |||
cf7eb88661 | |||
6a14ae7975 | |||
08f3294a1d | |||
ac47fc9295 | |||
1ff19e1467 | |||
439454a71b | |||
2a11964e1a | |||
507b8d43fb | |||
7efec281be | |||
9469f86f65 | |||
e998919097 | |||
450d69a1a4 | |||
b74681f22c | |||
f95a7c26e5 | |||
ffc9bd2cec | |||
bb7db0c828 | |||
aec3e08201 | |||
0651fbba06 | |||
bd9cd086a0 | |||
14fb0c3d61 | |||
c52afe5952 | |||
1d4b941a3b | |||
0344e5d9b3 | |||
d8e8cc062b |
@ -1,5 +1,5 @@
|
||||
[bumpversion]
|
||||
current_version = 2021.8.1-rc1
|
||||
current_version = 2021.8.1
|
||||
tag = True
|
||||
commit = True
|
||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-?(?P<release>.*)
|
||||
|
21
.github/workflows/release.yml
vendored
21
.github/workflows/release.yml
vendored
@ -33,14 +33,14 @@ jobs:
|
||||
with:
|
||||
push: ${{ github.event_name == 'release' }}
|
||||
tags: |
|
||||
beryju/authentik:2021.8.1-rc1,
|
||||
beryju/authentik:2021.8.1,
|
||||
beryju/authentik:latest,
|
||||
ghcr.io/goauthentik/server:2021.8.1-rc1,
|
||||
ghcr.io/goauthentik/server:2021.8.1,
|
||||
ghcr.io/goauthentik/server:latest
|
||||
platforms: linux/amd64,linux/arm64
|
||||
context: .
|
||||
- name: Building Docker Image (stable)
|
||||
if: ${{ github.event_name == 'release' && !contains('2021.8.1-rc1', 'rc') }}
|
||||
if: ${{ github.event_name == 'release' && !contains('2021.8.1', 'rc') }}
|
||||
run: |
|
||||
docker pull beryju/authentik:latest
|
||||
docker tag beryju/authentik:latest beryju/authentik:stable
|
||||
@ -75,14 +75,14 @@ jobs:
|
||||
with:
|
||||
push: ${{ github.event_name == 'release' }}
|
||||
tags: |
|
||||
beryju/authentik-proxy:2021.8.1-rc1,
|
||||
beryju/authentik-proxy:2021.8.1,
|
||||
beryju/authentik-proxy:latest,
|
||||
ghcr.io/goauthentik/proxy:2021.8.1-rc1,
|
||||
ghcr.io/goauthentik/proxy:2021.8.1,
|
||||
ghcr.io/goauthentik/proxy:latest
|
||||
file: proxy.Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
- name: Building Docker Image (stable)
|
||||
if: ${{ github.event_name == 'release' && !contains('2021.8.1-rc1', 'rc') }}
|
||||
if: ${{ github.event_name == 'release' && !contains('2021.8.1', 'rc') }}
|
||||
run: |
|
||||
docker pull beryju/authentik-proxy:latest
|
||||
docker tag beryju/authentik-proxy:latest beryju/authentik-proxy:stable
|
||||
@ -117,14 +117,14 @@ jobs:
|
||||
with:
|
||||
push: ${{ github.event_name == 'release' }}
|
||||
tags: |
|
||||
beryju/authentik-ldap:2021.8.1-rc1,
|
||||
beryju/authentik-ldap:2021.8.1,
|
||||
beryju/authentik-ldap:latest,
|
||||
ghcr.io/goauthentik/ldap:2021.8.1-rc1,
|
||||
ghcr.io/goauthentik/ldap:2021.8.1,
|
||||
ghcr.io/goauthentik/ldap:latest
|
||||
file: ldap.Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
- name: Building Docker Image (stable)
|
||||
if: ${{ github.event_name == 'release' && !contains('2021.8.1-rc1', 'rc') }}
|
||||
if: ${{ github.event_name == 'release' && !contains('2021.8.1', 'rc') }}
|
||||
run: |
|
||||
docker pull beryju/authentik-ldap:latest
|
||||
docker tag beryju/authentik-ldap:latest beryju/authentik-ldap:stable
|
||||
@ -163,7 +163,6 @@ jobs:
|
||||
- name: Build web api client and web ui
|
||||
run: |
|
||||
export NODE_ENV=production
|
||||
make gen-web
|
||||
cd web
|
||||
npm i
|
||||
npm run build
|
||||
@ -176,7 +175,7 @@ jobs:
|
||||
SENTRY_PROJECT: authentik
|
||||
SENTRY_URL: https://sentry.beryju.org
|
||||
with:
|
||||
version: authentik@2021.8.1-rc1
|
||||
version: authentik@2021.8.1
|
||||
environment: beryjuorg-prod
|
||||
sourcemaps: './web/dist'
|
||||
url_prefix: '~/static/dist'
|
||||
|
39
.github/workflows/web-api-publish.yml
vendored
Normal file
39
.github/workflows/web-api-publish.yml
vendored
Normal file
@ -0,0 +1,39 @@
|
||||
name: authentik-web-api-publish
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
paths:
|
||||
- 'schema.yml'
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
# Setup .npmrc file to publish to npm
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16.x'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
- name: Generate API Client
|
||||
run: make gen-web
|
||||
- name: Publish package
|
||||
run: |
|
||||
cd web-api/
|
||||
npm i
|
||||
npm publish
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}
|
||||
- name: Upgrade /web
|
||||
run: |
|
||||
cd web/
|
||||
export VERSION=`node -e 'console.log(require("../web-api/package.json").version)'`
|
||||
npm i @goauthentik/api@$VERSION
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v3
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
branch: update-web-api-client
|
||||
commit-message: "web: Update Web API Client version"
|
||||
title: "web: Update Web API Client version"
|
||||
delete-branch: true
|
||||
signoff: true
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -201,3 +201,4 @@ media/
|
||||
|
||||
.idea/
|
||||
/api/
|
||||
/web-api/
|
||||
|
22
.vscode/settings.json
vendored
Normal file
22
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"cSpell.words": [
|
||||
"asgi",
|
||||
"authentik",
|
||||
"authn",
|
||||
"goauthentik",
|
||||
"jwks",
|
||||
"oidc",
|
||||
"openid",
|
||||
"plex",
|
||||
"saml",
|
||||
"totp",
|
||||
"webauthn"
|
||||
],
|
||||
"python.linting.pylintEnabled": true,
|
||||
"todo-tree.tree.showCountsInTree": true,
|
||||
"todo-tree.tree.showBadges": true,
|
||||
"python.formatting.provider": "black",
|
||||
"files.associations": {
|
||||
"*.akflow": "json"
|
||||
}
|
||||
}
|
@ -11,8 +11,8 @@ The following is a set of guidelines for contributing to authentik and its compo
|
||||
[I don't want to read this whole thing, I just have a question!!!](#i-dont-want-to-read-this-whole-thing-i-just-have-a-question)
|
||||
|
||||
[What should I know before I get started?](#what-should-i-know-before-i-get-started)
|
||||
* [Atom and Packages](#atom-and-packages)
|
||||
* [Atom Design Decisions](#design-decisions)
|
||||
* [The components](#the-components)
|
||||
* [authentik's structure](#authentiks-structure)
|
||||
|
||||
[How Can I Contribute?](#how-can-i-contribute)
|
||||
* [Reporting Bugs](#reporting-bugs)
|
||||
@ -22,14 +22,9 @@ The following is a set of guidelines for contributing to authentik and its compo
|
||||
|
||||
[Styleguides](#styleguides)
|
||||
* [Git Commit Messages](#git-commit-messages)
|
||||
* [JavaScript Styleguide](#javascript-styleguide)
|
||||
* [CoffeeScript Styleguide](#coffeescript-styleguide)
|
||||
* [Specs Styleguide](#specs-styleguide)
|
||||
* [Python Styleguide](#python-styleguide)
|
||||
* [Documentation Styleguide](#documentation-styleguide)
|
||||
|
||||
[Additional Notes](#additional-notes)
|
||||
* [Issue and Pull Request Labels](#issue-and-pull-request-labels)
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
Basically, don't be a dickhead. This is an open-source non-profit project, that is made in the free time of Volunteers. If there's something you dislike or think can be done better, tell us! We'd love to hear any suggestions for improvement.
|
||||
|
15
Dockerfile
15
Dockerfile
@ -18,17 +18,6 @@ COPY ./website /static/
|
||||
ENV NODE_ENV=production
|
||||
RUN cd /static && npm i && npm run build-docs-only
|
||||
|
||||
# Stage 3: Build web API
|
||||
FROM openapitools/openapi-generator-cli as web-api-builder
|
||||
|
||||
COPY ./schema.yml /local/schema.yml
|
||||
|
||||
RUN docker-entrypoint.sh generate \
|
||||
-i /local/schema.yml \
|
||||
-g typescript-fetch \
|
||||
-o /local/web/api \
|
||||
--additional-properties=typescriptThreePlus=true,supportsES6=true,npmName=authentik-api,npmVersion=1.0.0
|
||||
|
||||
# Stage 3: Generate API Client
|
||||
FROM openapitools/openapi-generator-cli as go-api-builder
|
||||
|
||||
@ -48,7 +37,6 @@ RUN docker-entrypoint.sh generate \
|
||||
FROM node as web-builder
|
||||
|
||||
COPY ./web /static/
|
||||
COPY --from=web-api-builder /local/web/api /static/api
|
||||
|
||||
ENV NODE_ENV=production
|
||||
RUN cd /static && npm i && npm run build
|
||||
@ -110,4 +98,5 @@ COPY --from=builder /work/authentik /authentik-proxy
|
||||
USER authentik
|
||||
ENV TMPDIR /dev/shm/
|
||||
ENV PYTHONUBUFFERED 1
|
||||
ENTRYPOINT [ "/lifecycle/bootstrap.sh" ]
|
||||
ENV PATH "/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/lifecycle"
|
||||
ENTRYPOINT [ "/lifecycle/ak" ]
|
||||
|
12
Makefile
12
Makefile
@ -2,6 +2,7 @@
|
||||
PWD = $(shell pwd)
|
||||
UID = $(shell id -u)
|
||||
GID = $(shell id -g)
|
||||
NPM_VERSION = $(shell python -m scripts.npm_version)
|
||||
|
||||
all: lint-fix lint test gen
|
||||
|
||||
@ -41,10 +42,13 @@ gen-web:
|
||||
openapitools/openapi-generator-cli generate \
|
||||
-i /local/schema.yml \
|
||||
-g typescript-fetch \
|
||||
-o /local/web/api \
|
||||
--additional-properties=typescriptThreePlus=true,supportsES6=true,npmName=authentik-api,npmVersion=1.0.0
|
||||
# npm i runs tsc as part of the installation process
|
||||
cd web/api && npm i
|
||||
-o /local/web-api \
|
||||
--additional-properties=typescriptThreePlus=true,supportsES6=true,npmName=@goauthentik/api,npmVersion=${NPM_VERSION}
|
||||
mkdir -p web/node_modules/@goauthentik/api
|
||||
python -m scripts.web_api_esm
|
||||
\cp -fv scripts/web_api_readme.md web-api/README.md
|
||||
cd web-api && npm i
|
||||
\cp -rfv web-api/* web/node_modules/@goauthentik/api
|
||||
|
||||
gen-outpost:
|
||||
docker run \
|
||||
|
155
Pipfile.lock
generated
155
Pipfile.lock
generated
@ -122,19 +122,19 @@
|
||||
},
|
||||
"boto3": {
|
||||
"hashes": [
|
||||
"sha256:057196ac15de4de2221a24a3a0a41692414fa1dd697994d062ebd447163265e7",
|
||||
"sha256:852e776cea4287f74edcb45564f8345fb6b0168dde0fd5bf46668b94c3f21177"
|
||||
"sha256:4dc7e346e92c01e8a997daa58a4c990151841d2d2962067325d963f665c7287a",
|
||||
"sha256:79b7e6e0167def749352968ed6eb96954d9e2dd1dca8f297f122414753ce73a3"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.18.25"
|
||||
"version": "==1.18.29"
|
||||
},
|
||||
"botocore": {
|
||||
"hashes": [
|
||||
"sha256:201e10d3b1b40d65b7c9214be7087d78ed65de00e7362bd1e020741301d09fbc",
|
||||
"sha256:b9820ee29d70059c9b0e2a69ec13ebf80f4a0bc85f47578f17e951438c506b2d"
|
||||
"sha256:1f16998b4f5a88e6844196feee7fa5eef6b36034d377f9845c7df12b8803b3be",
|
||||
"sha256:fec924f63b40bd29b522fa109ecbc45f16eedcbeb22b68c6c79773c22a552b16"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==1.21.25"
|
||||
"version": "==1.21.29"
|
||||
},
|
||||
"cachetools": {
|
||||
"hashes": [
|
||||
@ -305,22 +305,25 @@
|
||||
},
|
||||
"cryptography": {
|
||||
"hashes": [
|
||||
"sha256:0f1212a66329c80d68aeeb39b8a16d54ef57071bf22ff4e521657b27372e327d",
|
||||
"sha256:1e056c28420c072c5e3cb36e2b23ee55e260cb04eee08f702e0edfec3fb51959",
|
||||
"sha256:240f5c21aef0b73f40bb9f78d2caff73186700bf1bc6b94285699aff98cc16c6",
|
||||
"sha256:26965837447f9c82f1855e0bc8bc4fb910240b6e0d16a664bb722df3b5b06873",
|
||||
"sha256:37340614f8a5d2fb9aeea67fd159bfe4f5f4ed535b1090ce8ec428b2f15a11f2",
|
||||
"sha256:3d10de8116d25649631977cb37da6cbdd2d6fa0e0281d014a5b7d337255ca713",
|
||||
"sha256:3d8427734c781ea5f1b41d6589c293089704d4759e34597dce91014ac125aad1",
|
||||
"sha256:7ec5d3b029f5fa2b179325908b9cd93db28ab7b85bb6c1db56b10e0b54235177",
|
||||
"sha256:8e56e16617872b0957d1c9742a3f94b43533447fd78321514abbe7db216aa250",
|
||||
"sha256:b01fd6f2737816cb1e08ed4807ae194404790eac7ad030b34f2ce72b332f5586",
|
||||
"sha256:bf40af59ca2465b24e54f671b2de2c59257ddc4f7e5706dbd6930e26823668d3",
|
||||
"sha256:de4e5f7f68220d92b7637fc99847475b59154b7a1b3868fb7385337af54ac9ca",
|
||||
"sha256:eb8cc2afe8b05acbd84a43905832ec78e7b3873fb124ca190f574dca7389a87d",
|
||||
"sha256:ee77aa129f481be46f8d92a1a7db57269a2f23052d5f2433b4621bb457081cc9"
|
||||
"sha256:0a7dcbcd3f1913f664aca35d47c1331fce738d44ec34b7be8b9d332151b0b01e",
|
||||
"sha256:1eb7bb0df6f6f583dd8e054689def236255161ebbcf62b226454ab9ec663746b",
|
||||
"sha256:21ca464b3a4b8d8e86ba0ee5045e103a1fcfac3b39319727bc0fc58c09c6aff7",
|
||||
"sha256:34dae04a0dce5730d8eb7894eab617d8a70d0c97da76b905de9efb7128ad7085",
|
||||
"sha256:3520667fda779eb788ea00080124875be18f2d8f0848ec00733c0ec3bb8219fc",
|
||||
"sha256:3fa3a7ccf96e826affdf1a0a9432be74dc73423125c8f96a909e3835a5ef194a",
|
||||
"sha256:5b0fbfae7ff7febdb74b574055c7466da334a5371f253732d7e2e7525d570498",
|
||||
"sha256:8695456444f277af73a4877db9fc979849cd3ee74c198d04fc0776ebc3db52b9",
|
||||
"sha256:94cc5ed4ceaefcbe5bf38c8fba6a21fc1d365bb8fb826ea1688e3370b2e24a1c",
|
||||
"sha256:94fff993ee9bc1b2440d3b7243d488c6a3d9724cc2b09cdb297f6a886d040ef7",
|
||||
"sha256:9965c46c674ba8cc572bc09a03f4c649292ee73e1b683adb1ce81e82e9a6a0fb",
|
||||
"sha256:a00cf305f07b26c351d8d4e1af84ad7501eca8a342dedf24a7acb0e7b7406e14",
|
||||
"sha256:a305600e7a6b7b855cd798e00278161b681ad6e9b7eca94c721d5f588ab212af",
|
||||
"sha256:cd65b60cfe004790c795cc35f272e41a3df4631e2fb6b35aa7ac6ef2859d554e",
|
||||
"sha256:d2a6e5ef66503da51d2110edf6c403dc6b494cc0082f85db12f54e9c5d4c3ec5",
|
||||
"sha256:d9ec0e67a14f9d1d48dd87a2531009a9b251c02ea42851c060b25c782516ff06",
|
||||
"sha256:f44d141b8c4ea5eb4dbc9b3ad992d45580c1d22bf5e24363f2fbf50c2d7ae8a7"
|
||||
],
|
||||
"version": "==3.4.7"
|
||||
"version": "==3.4.8"
|
||||
},
|
||||
"dacite": {
|
||||
"hashes": [
|
||||
@ -448,11 +451,11 @@
|
||||
},
|
||||
"drf-spectacular": {
|
||||
"hashes": [
|
||||
"sha256:f080128c42183fcaed6b9e8e5afcd2e5cd68426b1f80bfc85938f25e62db7fe5",
|
||||
"sha256:fb19aa69fcfcd37b0c9dfb9989c0671e1bb47af332ca2171378c7f840263788c"
|
||||
"sha256:5b1c27de127c86564be5a967a6fa195cfe161b552d98364282ae9e6ed3d75a85",
|
||||
"sha256:8588706c27f44adfbb3405bae9ef9cd6506f4b59d4cbd66c59780dce035602d9"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.17.3"
|
||||
"version": "==0.18.0"
|
||||
},
|
||||
"duo-client": {
|
||||
"hashes": [
|
||||
@ -810,11 +813,11 @@
|
||||
},
|
||||
"prompt-toolkit": {
|
||||
"hashes": [
|
||||
"sha256:08360ee3a3148bdb5163621709ee322ec34fc4375099afa4bbf751e9b7b7fa4f",
|
||||
"sha256:7089d8d2938043508aa9420ec18ce0922885304cddae87fb96eebca942299f88"
|
||||
"sha256:6076e46efae19b1e0ca1ec003ed37a933dc94b4d20f486235d436e64771dcd5c",
|
||||
"sha256:eb71d5a6b72ce6db177af4a7d4d7085b99756bf656d98ffcc4fecd36850eea6c"
|
||||
],
|
||||
"markers": "python_full_version >= '3.6.1'",
|
||||
"version": "==3.0.19"
|
||||
"markers": "python_full_version >= '3.6.2'",
|
||||
"version": "==3.0.20"
|
||||
},
|
||||
"psycopg2-binary": {
|
||||
"hashes": [
|
||||
@ -1417,11 +1420,11 @@
|
||||
},
|
||||
"astroid": {
|
||||
"hashes": [
|
||||
"sha256:3975a0bd5373bdce166e60c851cfcbaf21ee96de80ec518c1f4cb3e94c3fb334",
|
||||
"sha256:ab7f36e8a78b8e54a62028ba6beef7561db4cdb6f2a5009ecc44a6f42b5697ef"
|
||||
"sha256:b6c2d75cd7c2982d09e7d41d70213e863b3ba34d3bd4014e08f167cee966e99e",
|
||||
"sha256:ecc50f9b3803ebf8ea19aa2c6df5622d8a5c31456a53c741d3be044d96ff0948"
|
||||
],
|
||||
"markers": "python_version ~= '3.6'",
|
||||
"version": "==2.6.6"
|
||||
"version": "==2.7.2"
|
||||
},
|
||||
"attrs": {
|
||||
"hashes": [
|
||||
@ -1647,6 +1650,14 @@
|
||||
"markers": "python_version >= '2.6'",
|
||||
"version": "==5.6.0"
|
||||
},
|
||||
"platformdirs": {
|
||||
"hashes": [
|
||||
"sha256:4666d822218db6a262bdfdc9c39d21f23b4cfdb08af331a81e92751daf6c866c",
|
||||
"sha256:632daad3ab546bd8e6af0537d09805cec458dce201bccfe23012df73332e181e"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==2.2.0"
|
||||
},
|
||||
"pluggy": {
|
||||
"hashes": [
|
||||
"sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0",
|
||||
@ -1665,11 +1676,11 @@
|
||||
},
|
||||
"pylint": {
|
||||
"hashes": [
|
||||
"sha256:2e1a0eb2e8ab41d6b5dbada87f066492bb1557b12b76c47c2ee8aa8a11186594",
|
||||
"sha256:8b838c8983ee1904b2de66cce9d0b96649a91901350e956d78f289c3bc87b48e"
|
||||
"sha256:6758cce3ddbab60c52b57dcc07f0c5d779e5daf0cf50f6faacbef1d3ea62d2a1",
|
||||
"sha256:e178e96b6ba171f8ef51fbce9ca30931e6acbea4a155074d80cc081596c9e852"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.9.6"
|
||||
"version": "==2.10.2"
|
||||
},
|
||||
"pylint-django": {
|
||||
"hashes": [
|
||||
@ -1747,41 +1758,49 @@
|
||||
},
|
||||
"regex": {
|
||||
"hashes": [
|
||||
"sha256:026beb631097a4a3def7299aa5825e05e057de3c6d72b139c37813bfa351274b",
|
||||
"sha256:14caacd1853e40103f59571f169704367e79fb78fac3d6d09ac84d9197cadd16",
|
||||
"sha256:16d9eaa8c7e91537516c20da37db975f09ac2e7772a0694b245076c6d68f85da",
|
||||
"sha256:18fdc51458abc0a974822333bd3a932d4e06ba2a3243e9a1da305668bd62ec6d",
|
||||
"sha256:28e8af338240b6f39713a34e337c3813047896ace09d51593d6907c66c0708ba",
|
||||
"sha256:3835de96524a7b6869a6c710b26c90e94558c31006e96ca3cf6af6751b27dca1",
|
||||
"sha256:3905c86cc4ab6d71635d6419a6f8d972cab7c634539bba6053c47354fd04452c",
|
||||
"sha256:3c09d88a07483231119f5017904db8f60ad67906efac3f1baa31b9b7f7cca281",
|
||||
"sha256:4551728b767f35f86b8e5ec19a363df87450c7376d7419c3cac5b9ceb4bce576",
|
||||
"sha256:459bbe342c5b2dec5c5223e7c363f291558bc27982ef39ffd6569e8c082bdc83",
|
||||
"sha256:4f421e3cdd3a273bace013751c345f4ebeef08f05e8c10757533ada360b51a39",
|
||||
"sha256:577737ec3d4c195c4aef01b757905779a9e9aee608fa1cf0aec16b5576c893d3",
|
||||
"sha256:57fece29f7cc55d882fe282d9de52f2f522bb85290555b49394102f3621751ee",
|
||||
"sha256:7976d410e42be9ae7458c1816a416218364e06e162b82e42f7060737e711d9ce",
|
||||
"sha256:85f568892422a0e96235eb8ea6c5a41c8ccbf55576a2260c0160800dbd7c4f20",
|
||||
"sha256:8764a78c5464ac6bde91a8c87dd718c27c1cabb7ed2b4beaf36d3e8e390567f9",
|
||||
"sha256:8935937dad2c9b369c3d932b0edbc52a62647c2afb2fafc0c280f14a8bf56a6a",
|
||||
"sha256:8fe58d9f6e3d1abf690174fd75800fda9bdc23d2a287e77758dc0e8567e38ce6",
|
||||
"sha256:937b20955806381e08e54bd9d71f83276d1f883264808521b70b33d98e4dec5d",
|
||||
"sha256:9569da9e78f0947b249370cb8fadf1015a193c359e7e442ac9ecc585d937f08d",
|
||||
"sha256:a3b73390511edd2db2d34ff09aa0b2c08be974c71b4c0505b4a048d5dc128c2b",
|
||||
"sha256:a4eddbe2a715b2dd3849afbdeacf1cc283160b24e09baf64fa5675f51940419d",
|
||||
"sha256:a5c6dbe09aff091adfa8c7cfc1a0e83fdb8021ddb2c183512775a14f1435fe16",
|
||||
"sha256:b63e3571b24a7959017573b6455e05b675050bbbea69408f35f3cb984ec54363",
|
||||
"sha256:bb350eb1060591d8e89d6bac4713d41006cd4d479f5e11db334a48ff8999512f",
|
||||
"sha256:bf6d987edd4a44dd2fa2723fca2790f9442ae4de2c8438e53fcb1befdf5d823a",
|
||||
"sha256:bfa6a679410b394600eafd16336b2ce8de43e9b13f7fb9247d84ef5ad2b45e91",
|
||||
"sha256:c856ec9b42e5af4fe2d8e75970fcc3a2c15925cbcc6e7a9bcb44583b10b95e80",
|
||||
"sha256:cea56288eeda8b7511d507bbe7790d89ae7049daa5f51ae31a35ae3c05408531",
|
||||
"sha256:ea212df6e5d3f60341aef46401d32fcfded85593af1d82b8b4a7a68cd67fdd6b",
|
||||
"sha256:f35567470ee6dbfb946f069ed5f5615b40edcbb5f1e6e1d3d2b114468d505fc6",
|
||||
"sha256:fbc20975eee093efa2071de80df7f972b7b35e560b213aafabcec7c0bd00bd8c",
|
||||
"sha256:ff4a8ad9638b7ca52313d8732f37ecd5fd3c8e3aff10a8ccb93176fd5b3812f6"
|
||||
"sha256:03840a07a402576b8e3a6261f17eb88abd653ad4e18ec46ef10c9a63f8c99ebd",
|
||||
"sha256:06ba444bbf7ede3890a912bd4904bb65bf0da8f0d8808b90545481362c978642",
|
||||
"sha256:1f9974826aeeda32a76648fc677e3125ade379869a84aa964b683984a2dea9f1",
|
||||
"sha256:330836ad89ff0be756b58758878409f591d4737b6a8cef26a162e2a4961c3321",
|
||||
"sha256:38600fd58c2996829480de7d034fb2d3a0307110e44dae80b6b4f9b3d2eea529",
|
||||
"sha256:3a195e26df1fbb40ebee75865f9b64ba692a5824ecb91c078cc665b01f7a9a36",
|
||||
"sha256:41acdd6d64cd56f857e271009966c2ffcbd07ec9149ca91f71088574eaa4278a",
|
||||
"sha256:45f97ade892ace20252e5ccecdd7515c7df5feeb42c3d2a8b8c55920c3551c30",
|
||||
"sha256:4b0c211c55d4aac4309c3209833c803fada3fc21cdf7b74abedda42a0c9dc3ce",
|
||||
"sha256:5d5209c3ba25864b1a57461526ebde31483db295fc6195fdfc4f8355e10f7376",
|
||||
"sha256:615fb5a524cffc91ab4490b69e10ae76c1ccbfa3383ea2fad72e54a85c7d47dd",
|
||||
"sha256:61e734c2bcb3742c3f454dfa930ea60ea08f56fd1a0eb52d8cb189a2f6be9586",
|
||||
"sha256:640ccca4d0a6fcc6590f005ecd7b16c3d8f5d52174e4854f96b16f34c39d6cb7",
|
||||
"sha256:6dbd51c3db300ce9d3171f4106da18fe49e7045232630fe3d4c6e37cb2b39ab9",
|
||||
"sha256:71a904da8c9c02aee581f4452a5a988c3003207cb8033db426f29e5b2c0b7aea",
|
||||
"sha256:8021dee64899f993f4b5cca323aae65aabc01a546ed44356a0965e29d7893c94",
|
||||
"sha256:8b8d551f1bd60b3e1c59ff55b9e8d74607a5308f66e2916948cafd13480b44a3",
|
||||
"sha256:93f9f720081d97acee38a411e861d4ce84cbc8ea5319bc1f8e38c972c47af49f",
|
||||
"sha256:96f0c79a70642dfdf7e6a018ebcbea7ea5205e27d8e019cad442d2acfc9af267",
|
||||
"sha256:9966337353e436e6ba652814b0a957a517feb492a98b8f9d3b6ba76d22301dcc",
|
||||
"sha256:a34ba9e39f8269fd66ab4f7a802794ffea6d6ac500568ec05b327a862c21ce23",
|
||||
"sha256:a49f85f0a099a5755d0a2cc6fc337e3cb945ad6390ec892332c691ab0a045882",
|
||||
"sha256:a795829dc522227265d72b25d6ee6f6d41eb2105c15912c230097c8f5bfdbcdc",
|
||||
"sha256:a89ca4105f8099de349d139d1090bad387fe2b208b717b288699ca26f179acbe",
|
||||
"sha256:ac95101736239260189f426b1e361dc1b704513963357dc474beb0f39f5b7759",
|
||||
"sha256:ae87ab669431f611c56e581679db33b9a467f87d7bf197ac384e71e4956b4456",
|
||||
"sha256:b091dcfee169ad8de21b61eb2c3a75f9f0f859f851f64fdaf9320759a3244239",
|
||||
"sha256:b511c6009d50d5c0dd0bab85ed25bc8ad6b6f5611de3a63a59786207e82824bb",
|
||||
"sha256:b79dc2b2e313565416c1e62807c7c25c67a6ff0a0f8d83a318df464555b65948",
|
||||
"sha256:bca14dfcfd9aae06d7d8d7e105539bd77d39d06caaae57a1ce945670bae744e0",
|
||||
"sha256:c835c30f3af5c63a80917b72115e1defb83de99c73bc727bddd979a3b449e183",
|
||||
"sha256:ccd721f1d4fc42b541b633d6e339018a08dd0290dc67269df79552843a06ca92",
|
||||
"sha256:d6c2b1d78ceceb6741d703508cd0e9197b34f6bf6864dab30f940f8886e04ade",
|
||||
"sha256:d6ec4ae13760ceda023b2e5ef1f9bc0b21e4b0830458db143794a117fdbdc044",
|
||||
"sha256:d8b623fc429a38a881ab2d9a56ef30e8ea20c72a891c193f5ebbddc016e083ee",
|
||||
"sha256:ea9753d64cba6f226947c318a923dadaf1e21cd8db02f71652405263daa1f033",
|
||||
"sha256:ebbceefbffae118ab954d3cd6bf718f5790db66152f95202ebc231d58ad4e2c2",
|
||||
"sha256:ecb6e7c45f9cd199c10ec35262b53b2247fb9a408803ed00ee5bb2b54aa626f5",
|
||||
"sha256:ef9326c64349e2d718373415814e754183057ebc092261387a2c2f732d9172b2",
|
||||
"sha256:f93a9d8804f4cec9da6c26c8cfae2c777028b4fdd9f49de0302e26e00bb86504",
|
||||
"sha256:faf08b0341828f6a29b8f7dd94d5cf8cc7c39bfc3e67b78514c54b494b66915a"
|
||||
],
|
||||
"version": "==2021.8.3"
|
||||
"version": "==2021.8.21"
|
||||
},
|
||||
"requests": {
|
||||
"hashes": [
|
||||
|
@ -1,3 +1,3 @@
|
||||
"""authentik"""
|
||||
__version__ = "2021.8.1-rc1"
|
||||
__version__ = "2021.8.1"
|
||||
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
||||
|
@ -34,6 +34,7 @@ class RuntimeDict(TypedDict):
|
||||
class SystemSerializer(PassiveSerializer):
|
||||
"""Get system information."""
|
||||
|
||||
env = SerializerMethodField()
|
||||
http_headers = SerializerMethodField()
|
||||
http_host = SerializerMethodField()
|
||||
http_is_secure = SerializerMethodField()
|
||||
@ -42,6 +43,10 @@ class SystemSerializer(PassiveSerializer):
|
||||
server_time = SerializerMethodField()
|
||||
embedded_outpost_host = SerializerMethodField()
|
||||
|
||||
def get_env(self, request: Request) -> dict[str, str]:
|
||||
"""Get Environment"""
|
||||
return os.environ.copy()
|
||||
|
||||
def get_http_headers(self, request: Request) -> dict[str, str]:
|
||||
"""Get HTTP Request headers"""
|
||||
headers = {}
|
||||
|
@ -33,7 +33,7 @@ def bearer_auth(raw_header: bytes) -> Optional[User]:
|
||||
raise AuthenticationFailed("Malformed header")
|
||||
# Accept credentials with username and without
|
||||
if ":" in auth_credentials:
|
||||
_, password = auth_credentials.split(":")
|
||||
_, _, password = auth_credentials.partition(":")
|
||||
else:
|
||||
password = auth_credentials
|
||||
if password == "": # nosec
|
||||
|
@ -4,14 +4,9 @@ from django.db.models import QuerySet
|
||||
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,
|
||||
inline_serializer,
|
||||
)
|
||||
from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.fields import BooleanField, CharField, FileField, ReadOnlyField
|
||||
from rest_framework.fields import ReadOnlyField
|
||||
from rest_framework.parsers import MultiPartParser
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
@ -24,6 +19,7 @@ from authentik.admin.api.metrics import CoordinateSerializer, get_events_per_1h
|
||||
from authentik.api.decorators import permission_required
|
||||
from authentik.core.api.providers import ProviderSerializer
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.core.api.utils import FilePathSerializer, FileUploadSerializer
|
||||
from authentik.core.models import Application, User
|
||||
from authentik.events.models import EventAction
|
||||
from authentik.policies.api.exec import PolicyTestResultSerializer
|
||||
@ -122,7 +118,10 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet):
|
||||
# If the current user is superuser, they can set `for_user`
|
||||
for_user = request.user
|
||||
if request.user.is_superuser and "for_user" in request.query_params:
|
||||
for_user = get_object_or_404(User, pk=request.query_params.get("for_user"))
|
||||
try:
|
||||
for_user = get_object_or_404(User, pk=request.query_params.get("for_user"))
|
||||
except ValueError:
|
||||
return HttpResponseBadRequest("for_user must be numerical")
|
||||
engine = PolicyEngine(application, for_user, request)
|
||||
engine.use_cache = False
|
||||
engine.build()
|
||||
@ -177,13 +176,7 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet):
|
||||
@permission_required("authentik_core.change_application")
|
||||
@extend_schema(
|
||||
request={
|
||||
"multipart/form-data": inline_serializer(
|
||||
"SetIcon",
|
||||
fields={
|
||||
"file": FileField(required=False),
|
||||
"clear": BooleanField(default=False),
|
||||
},
|
||||
)
|
||||
"multipart/form-data": FileUploadSerializer,
|
||||
},
|
||||
responses={
|
||||
200: OpenApiResponse(description="Success"),
|
||||
@ -215,7 +208,7 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet):
|
||||
|
||||
@permission_required("authentik_core.change_application")
|
||||
@extend_schema(
|
||||
request=inline_serializer("SetIconURL", fields={"url": CharField()}),
|
||||
request=FilePathSerializer,
|
||||
responses={
|
||||
200: OpenApiResponse(description="Success"),
|
||||
400: OpenApiResponse(description="Bad request"),
|
||||
|
@ -1,7 +1,10 @@
|
||||
"""Tokens API Viewset"""
|
||||
from typing import Any
|
||||
|
||||
from django.http.response import Http404
|
||||
from drf_spectacular.utils import OpenApiResponse, extend_schema
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.fields import CharField
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
@ -20,7 +23,16 @@ from authentik.managed.api import ManagedSerializer
|
||||
class TokenSerializer(ManagedSerializer, ModelSerializer):
|
||||
"""Token Serializer"""
|
||||
|
||||
user = UserSerializer(required=False)
|
||||
user_obj = UserSerializer(required=False)
|
||||
|
||||
def validate(self, attrs: dict[Any, str]) -> dict[Any, str]:
|
||||
"""Ensure only API or App password tokens are created."""
|
||||
request: Request = self.context["request"]
|
||||
attrs.setdefault("user", request.user)
|
||||
attrs.setdefault("intent", TokenIntents.INTENT_API)
|
||||
if attrs.get("intent") not in [TokenIntents.INTENT_API, TokenIntents.INTENT_APP_PASSWORD]:
|
||||
raise ValidationError(f"Invalid intent {attrs.get('intent')}")
|
||||
return attrs
|
||||
|
||||
class Meta:
|
||||
|
||||
@ -31,11 +43,14 @@ class TokenSerializer(ManagedSerializer, ModelSerializer):
|
||||
"identifier",
|
||||
"intent",
|
||||
"user",
|
||||
"user_obj",
|
||||
"description",
|
||||
"expires",
|
||||
"expiring",
|
||||
]
|
||||
depth = 2
|
||||
extra_kwargs = {
|
||||
"user": {"required": False},
|
||||
}
|
||||
|
||||
|
||||
class TokenViewSerializer(PassiveSerializer):
|
||||
@ -69,7 +84,6 @@ class TokenViewSet(UsedByMixin, ModelViewSet):
|
||||
def perform_create(self, serializer: TokenSerializer):
|
||||
serializer.save(
|
||||
user=self.request.user,
|
||||
intent=TokenIntents.INTENT_API,
|
||||
expiring=self.request.user.attributes.get(USER_ATTRIBUTE_TOKEN_EXPIRING, True),
|
||||
)
|
||||
|
||||
|
@ -3,13 +3,20 @@ from json import loads
|
||||
from typing import Optional
|
||||
|
||||
from django.db.models.query import QuerySet
|
||||
from django.db.transaction import atomic
|
||||
from django.db.utils import IntegrityError
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.http import urlencode
|
||||
from django.utils.translation import gettext as _
|
||||
from django_filters.filters import BooleanFilter, CharFilter, ModelMultipleChoiceFilter
|
||||
from django_filters.filterset import FilterSet
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from drf_spectacular.utils import OpenApiParameter, extend_schema, extend_schema_field
|
||||
from drf_spectacular.utils import (
|
||||
OpenApiParameter,
|
||||
extend_schema,
|
||||
extend_schema_field,
|
||||
inline_serializer,
|
||||
)
|
||||
from guardian.shortcuts import get_anonymous_user, get_objects_for_user
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.fields import CharField, JSONField, SerializerMethodField
|
||||
@ -34,7 +41,14 @@ from authentik.core.api.groups import GroupSerializer
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.core.api.utils import LinkSerializer, PassiveSerializer, is_dict
|
||||
from authentik.core.middleware import SESSION_IMPERSONATE_ORIGINAL_USER, SESSION_IMPERSONATE_USER
|
||||
from authentik.core.models import Group, Token, TokenIntents, User
|
||||
from authentik.core.models import (
|
||||
USER_ATTRIBUTE_SA,
|
||||
USER_ATTRIBUTE_TOKEN_EXPIRING,
|
||||
Group,
|
||||
Token,
|
||||
TokenIntents,
|
||||
User,
|
||||
)
|
||||
from authentik.events.models import EventAction
|
||||
from authentik.stages.email.models import EmailStage
|
||||
from authentik.stages.email.tasks import send_mails
|
||||
@ -220,6 +234,51 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
||||
)
|
||||
return link, token
|
||||
|
||||
@permission_required(None, ["authentik_core.add_user", "authentik_core.add_token"])
|
||||
@extend_schema(
|
||||
request=inline_serializer(
|
||||
"UserServiceAccountSerializer",
|
||||
{
|
||||
"name": CharField(required=True),
|
||||
"create_group": BooleanField(default=False),
|
||||
},
|
||||
),
|
||||
responses={
|
||||
200: inline_serializer(
|
||||
"UserServiceAccountResponse",
|
||||
{
|
||||
"username": CharField(required=True),
|
||||
"token": CharField(required=True),
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
@action(detail=False, methods=["POST"], pagination_class=None, filter_backends=[])
|
||||
def service_account(self, request: Request) -> Response:
|
||||
"""Create a new user account that is marked as a service account"""
|
||||
username = request.data.get("name")
|
||||
create_group = request.data.get("create_group", False)
|
||||
with atomic():
|
||||
try:
|
||||
user = User.objects.create(
|
||||
username=username,
|
||||
name=username,
|
||||
attributes={USER_ATTRIBUTE_SA: True, USER_ATTRIBUTE_TOKEN_EXPIRING: False},
|
||||
)
|
||||
if create_group:
|
||||
group = Group.objects.create(
|
||||
name=username,
|
||||
)
|
||||
group.users.add(user)
|
||||
token = Token.objects.create(
|
||||
identifier=f"service-account-{username}-password",
|
||||
intent=TokenIntents.INTENT_APP_PASSWORD,
|
||||
user=user,
|
||||
)
|
||||
return Response({"username": user.username, "token": token.key})
|
||||
except (IntegrityError) as exc:
|
||||
return Response(data={"non_field_errors": [str(exc)]}, status=400)
|
||||
|
||||
@extend_schema(responses={200: SessionUserSerializer(many=False)})
|
||||
@action(detail=False, pagination_class=None, filter_backends=[])
|
||||
# pylint: disable=invalid-name
|
||||
@ -251,7 +310,9 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
||||
# since it caches the full object
|
||||
if SESSION_IMPERSONATE_USER in request.session:
|
||||
request.session[SESSION_IMPERSONATE_USER] = new_user
|
||||
return self.me(request)
|
||||
serializer = SessionUserSerializer(data={"user": UserSelfSerializer(request.user).data})
|
||||
serializer.is_valid()
|
||||
return Response(serializer.data)
|
||||
|
||||
@permission_required("authentik_core.view_user", ["authentik_events.view_event"])
|
||||
@extend_schema(responses={200: UserMetricsSerializer(many=False)})
|
||||
|
@ -2,7 +2,7 @@
|
||||
from typing import Any
|
||||
|
||||
from django.db.models import Model
|
||||
from rest_framework.fields import CharField, IntegerField
|
||||
from rest_framework.fields import BooleanField, CharField, FileField, IntegerField
|
||||
from rest_framework.serializers import Serializer, SerializerMethodField, ValidationError
|
||||
|
||||
|
||||
@ -22,8 +22,18 @@ class PassiveSerializer(Serializer):
|
||||
def update(self, instance: Model, validated_data: dict) -> Model: # pragma: no cover
|
||||
return Model()
|
||||
|
||||
class Meta:
|
||||
model = Model
|
||||
|
||||
class FileUploadSerializer(PassiveSerializer):
|
||||
"""Serializer to upload file"""
|
||||
|
||||
file = FileField(required=False)
|
||||
clear = BooleanField(default=False)
|
||||
|
||||
|
||||
class FilePathSerializer(PassiveSerializer):
|
||||
"""Serializer to upload file"""
|
||||
|
||||
url = CharField()
|
||||
|
||||
|
||||
class MetaNameSerializer(PassiveSerializer):
|
||||
|
59
authentik/core/auth.py
Normal file
59
authentik/core/auth.py
Normal file
@ -0,0 +1,59 @@
|
||||
"""Authenticate with tokens"""
|
||||
|
||||
from typing import Any, Optional
|
||||
|
||||
from django.contrib.auth.backends import ModelBackend
|
||||
from django.http.request import HttpRequest
|
||||
|
||||
from authentik.core.models import Token, TokenIntents, User
|
||||
from authentik.events.utils import cleanse_dict, sanitize_dict
|
||||
from authentik.flows.planner import FlowPlan
|
||||
from authentik.flows.views import SESSION_KEY_PLAN
|
||||
from authentik.stages.password.stage import PLAN_CONTEXT_METHOD, PLAN_CONTEXT_METHOD_ARGS
|
||||
|
||||
|
||||
class InbuiltBackend(ModelBackend):
|
||||
"""Inbuilt backend"""
|
||||
|
||||
def authenticate(
|
||||
self, request: HttpRequest, username: Optional[str], password: Optional[str], **kwargs: Any
|
||||
) -> Optional[User]:
|
||||
user = super().authenticate(request, username=username, password=password, **kwargs)
|
||||
if not user:
|
||||
return None
|
||||
self.set_method("password", request)
|
||||
return user
|
||||
|
||||
def set_method(self, method: str, request: Optional[HttpRequest], **kwargs):
|
||||
"""Set method data on current flow, if possbiel"""
|
||||
if not request:
|
||||
return
|
||||
# Since we can't directly pass other variables to signals, and we want to log the method
|
||||
# and the token used, we assume we're running in a flow and set a variable in the context
|
||||
flow_plan: FlowPlan = request.session[SESSION_KEY_PLAN]
|
||||
flow_plan.context[PLAN_CONTEXT_METHOD] = method
|
||||
flow_plan.context[PLAN_CONTEXT_METHOD_ARGS] = cleanse_dict(sanitize_dict(kwargs))
|
||||
request.session[SESSION_KEY_PLAN] = flow_plan
|
||||
|
||||
|
||||
class TokenBackend(InbuiltBackend):
|
||||
"""Authenticate with token"""
|
||||
|
||||
def authenticate(
|
||||
self, request: HttpRequest, username: Optional[str], password: Optional[str], **kwargs: Any
|
||||
) -> Optional[User]:
|
||||
try:
|
||||
user = User._default_manager.get_by_natural_key(username)
|
||||
except User.DoesNotExist:
|
||||
# Run the default password hasher once to reduce the timing
|
||||
# difference between an existing and a nonexistent user (#20760).
|
||||
User().set_password(password)
|
||||
return None
|
||||
tokens = Token.filter_not_expired(
|
||||
user=user, key=password, intent=TokenIntents.INTENT_APP_PASSWORD
|
||||
)
|
||||
if not tokens.exists():
|
||||
return None
|
||||
token = tokens.first()
|
||||
self.set_method("password", request, token=token)
|
||||
return token.user
|
26
authentik/core/migrations/0028_alter_token_intent.py
Normal file
26
authentik/core/migrations/0028_alter_token_intent.py
Normal file
@ -0,0 +1,26 @@
|
||||
# Generated by Django 3.2.6 on 2021-08-23 14:35
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_core", "0027_bootstrap_token"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="token",
|
||||
name="intent",
|
||||
field=models.TextField(
|
||||
choices=[
|
||||
("verification", "Intent Verification"),
|
||||
("api", "Intent Api"),
|
||||
("recovery", "Intent Recovery"),
|
||||
("app_password", "Intent App Password"),
|
||||
],
|
||||
default="verification",
|
||||
),
|
||||
),
|
||||
]
|
@ -28,6 +28,7 @@ from authentik.core.signals import password_changed
|
||||
from authentik.core.types import UILoginButton, UserSettingSerializer
|
||||
from authentik.flows.models import Flow
|
||||
from authentik.lib.config import CONFIG
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.lib.models import CreatedUpdatedModel, SerializerModel
|
||||
from authentik.lib.utils.http import get_client_ip
|
||||
from authentik.managed.models import ManagedModel
|
||||
@ -54,7 +55,9 @@ def default_token_duration():
|
||||
|
||||
def default_token_key():
|
||||
"""Default token key"""
|
||||
return uuid4().hex
|
||||
# We use generate_id since the chars in the key should be easy
|
||||
# to use in Emails (for verification) and URLs (for recovery)
|
||||
return generate_id(128)
|
||||
|
||||
|
||||
class Group(models.Model):
|
||||
@ -408,6 +411,9 @@ class TokenIntents(models.TextChoices):
|
||||
# Recovery use for the recovery app
|
||||
INTENT_RECOVERY = "recovery"
|
||||
|
||||
# App-specific passwords
|
||||
INTENT_APP_PASSWORD = "app_password" # nosec
|
||||
|
||||
|
||||
class Token(ManagedModel, ExpiringModel):
|
||||
"""Token used to authenticate the User for API Access or confirm another Stage like Email."""
|
||||
|
@ -25,7 +25,7 @@ from authentik.flows.planner import (
|
||||
from authentik.flows.views import NEXT_ARG_NAME, SESSION_KEY_GET, SESSION_KEY_PLAN
|
||||
from authentik.lib.utils.urls import redirect_with_qs
|
||||
from authentik.policies.utils import delete_none_keys
|
||||
from authentik.stages.password import BACKEND_DJANGO
|
||||
from authentik.stages.password import BACKEND_INBUILT
|
||||
from authentik.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND
|
||||
from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
|
||||
|
||||
@ -189,7 +189,7 @@ class SourceFlowManager:
|
||||
kwargs.update(
|
||||
{
|
||||
# Since we authenticate the user by their token, they have no backend set
|
||||
PLAN_CONTEXT_AUTHENTICATION_BACKEND: BACKEND_DJANGO,
|
||||
PLAN_CONTEXT_AUTHENTICATION_BACKEND: BACKEND_INBUILT,
|
||||
PLAN_CONTEXT_SSO: True,
|
||||
PLAN_CONTEXT_SOURCE: self.source,
|
||||
PLAN_CONTEXT_REDIRECT: final_redirect,
|
||||
|
@ -1,16 +1,13 @@
|
||||
"""Test Source flow_manager"""
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.contrib.messages.middleware import MessageMiddleware
|
||||
from django.contrib.sessions.middleware import SessionMiddleware
|
||||
from django.http.request import HttpRequest
|
||||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
from guardian.utils import get_anonymous_user
|
||||
|
||||
from authentik.core.models import SourceUserMatchingModes, User
|
||||
from authentik.core.sources.flow_manager import Action
|
||||
from authentik.flows.tests.test_planner import dummy_get_response
|
||||
from authentik.providers.oauth2.generators import generate_client_id
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.lib.tests.utils import get_request
|
||||
from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection
|
||||
from authentik.sources.oauth.views.callback import OAuthSourceFlowManager
|
||||
|
||||
@ -22,24 +19,12 @@ class TestSourceFlowManager(TestCase):
|
||||
super().setUp()
|
||||
self.source = OAuthSource.objects.create(name="test")
|
||||
self.factory = RequestFactory()
|
||||
self.identifier = generate_client_id()
|
||||
|
||||
def get_request(self, user: User) -> HttpRequest:
|
||||
"""Helper to create a get request with session and message middleware"""
|
||||
request = self.factory.get("/")
|
||||
request.user = user
|
||||
middleware = SessionMiddleware(dummy_get_response)
|
||||
middleware.process_request(request)
|
||||
request.session.save()
|
||||
middleware = MessageMiddleware(dummy_get_response)
|
||||
middleware.process_request(request)
|
||||
request.session.save()
|
||||
return request
|
||||
self.identifier = generate_id()
|
||||
|
||||
def test_unauthenticated_enroll(self):
|
||||
"""Test un-authenticated user enrolling"""
|
||||
flow_manager = OAuthSourceFlowManager(
|
||||
self.source, self.get_request(AnonymousUser()), self.identifier, {}
|
||||
self.source, get_request("/", user=AnonymousUser()), self.identifier, {}
|
||||
)
|
||||
action, _ = flow_manager.get_action()
|
||||
self.assertEqual(action, Action.ENROLL)
|
||||
@ -52,7 +37,7 @@ class TestSourceFlowManager(TestCase):
|
||||
)
|
||||
|
||||
flow_manager = OAuthSourceFlowManager(
|
||||
self.source, self.get_request(AnonymousUser()), self.identifier, {}
|
||||
self.source, get_request("/", user=AnonymousUser()), self.identifier, {}
|
||||
)
|
||||
action, _ = flow_manager.get_action()
|
||||
self.assertEqual(action, Action.AUTH)
|
||||
@ -65,7 +50,7 @@ class TestSourceFlowManager(TestCase):
|
||||
)
|
||||
user = User.objects.create(username="foo", email="foo@bar.baz")
|
||||
flow_manager = OAuthSourceFlowManager(
|
||||
self.source, self.get_request(user), self.identifier, {}
|
||||
self.source, get_request("/", user=user), self.identifier, {}
|
||||
)
|
||||
action, _ = flow_manager.get_action()
|
||||
self.assertEqual(action, Action.LINK)
|
||||
@ -78,7 +63,7 @@ class TestSourceFlowManager(TestCase):
|
||||
|
||||
# Without email, deny
|
||||
flow_manager = OAuthSourceFlowManager(
|
||||
self.source, self.get_request(AnonymousUser()), self.identifier, {}
|
||||
self.source, get_request("/", user=AnonymousUser()), self.identifier, {}
|
||||
)
|
||||
action, _ = flow_manager.get_action()
|
||||
self.assertEqual(action, Action.DENY)
|
||||
@ -86,7 +71,7 @@ class TestSourceFlowManager(TestCase):
|
||||
# With email
|
||||
flow_manager = OAuthSourceFlowManager(
|
||||
self.source,
|
||||
self.get_request(AnonymousUser()),
|
||||
get_request("/", user=AnonymousUser()),
|
||||
self.identifier,
|
||||
{"email": "foo@bar.baz"},
|
||||
)
|
||||
@ -101,7 +86,7 @@ class TestSourceFlowManager(TestCase):
|
||||
|
||||
# Without username, deny
|
||||
flow_manager = OAuthSourceFlowManager(
|
||||
self.source, self.get_request(AnonymousUser()), self.identifier, {}
|
||||
self.source, get_request("/", user=AnonymousUser()), self.identifier, {}
|
||||
)
|
||||
action, _ = flow_manager.get_action()
|
||||
self.assertEqual(action, Action.DENY)
|
||||
@ -109,7 +94,7 @@ class TestSourceFlowManager(TestCase):
|
||||
# With username
|
||||
flow_manager = OAuthSourceFlowManager(
|
||||
self.source,
|
||||
self.get_request(AnonymousUser()),
|
||||
get_request("/", user=AnonymousUser()),
|
||||
self.identifier,
|
||||
{"username": "foo"},
|
||||
)
|
||||
@ -125,7 +110,7 @@ class TestSourceFlowManager(TestCase):
|
||||
# With non-existent username, enroll
|
||||
flow_manager = OAuthSourceFlowManager(
|
||||
self.source,
|
||||
self.get_request(AnonymousUser()),
|
||||
get_request("/", user=AnonymousUser()),
|
||||
self.identifier,
|
||||
{
|
||||
"username": "bar",
|
||||
@ -137,7 +122,7 @@ class TestSourceFlowManager(TestCase):
|
||||
# With username
|
||||
flow_manager = OAuthSourceFlowManager(
|
||||
self.source,
|
||||
self.get_request(AnonymousUser()),
|
||||
get_request("/", user=AnonymousUser()),
|
||||
self.identifier,
|
||||
{"username": "foo"},
|
||||
)
|
||||
@ -151,7 +136,7 @@ class TestSourceFlowManager(TestCase):
|
||||
|
||||
flow_manager = OAuthSourceFlowManager(
|
||||
self.source,
|
||||
self.get_request(AnonymousUser()),
|
||||
get_request("/", user=AnonymousUser()),
|
||||
self.identifier,
|
||||
{"username": "foo"},
|
||||
)
|
||||
|
@ -27,6 +27,14 @@ class TestTokenAPI(APITestCase):
|
||||
self.assertEqual(token.intent, TokenIntents.INTENT_API)
|
||||
self.assertEqual(token.expiring, True)
|
||||
|
||||
def test_token_create_invalid(self):
|
||||
"""Test token creation endpoint (invalid data)"""
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:token-list"),
|
||||
{"identifier": "test-token", "intent": TokenIntents.INTENT_RECOVERY},
|
||||
)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_token_create_non_expiring(self):
|
||||
"""Test token creation endpoint"""
|
||||
self.user.attributes[USER_ATTRIBUTE_TOKEN_EXPIRING] = False
|
||||
|
40
authentik/core/tests/test_token_auth.py
Normal file
40
authentik/core/tests/test_token_auth.py
Normal file
@ -0,0 +1,40 @@
|
||||
"""Test token auth"""
|
||||
from django.test import TestCase
|
||||
|
||||
from authentik.core.auth import TokenBackend
|
||||
from authentik.core.models import Token, TokenIntents, User
|
||||
from authentik.flows.planner import FlowPlan
|
||||
from authentik.flows.views import SESSION_KEY_PLAN
|
||||
from authentik.lib.tests.utils import get_request
|
||||
|
||||
|
||||
class TestTokenAuth(TestCase):
|
||||
"""Test token auth"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.user = User.objects.create(username="test-user")
|
||||
self.token = Token.objects.create(
|
||||
expiring=False, user=self.user, intent=TokenIntents.INTENT_APP_PASSWORD
|
||||
)
|
||||
# To test with session we need to create a request and pass it through all middlewares
|
||||
self.request = get_request("/")
|
||||
self.request.session[SESSION_KEY_PLAN] = FlowPlan("test")
|
||||
|
||||
def test_token_auth(self):
|
||||
"""Test auth with token"""
|
||||
self.assertEqual(
|
||||
TokenBackend().authenticate(self.request, "test-user", self.token.key), self.user
|
||||
)
|
||||
|
||||
def test_token_auth_none(self):
|
||||
"""Test auth with token (non-existent user)"""
|
||||
self.assertIsNone(
|
||||
TokenBackend().authenticate(self.request, "test-user-foo", self.token.key), self.user
|
||||
)
|
||||
|
||||
def test_token_auth_invalid(self):
|
||||
"""Test auth with token (invalid token)"""
|
||||
self.assertIsNone(
|
||||
TokenBackend().authenticate(self.request, "test-user", self.token.key + "foo"),
|
||||
self.user,
|
||||
)
|
@ -105,3 +105,39 @@ class TestUsersAPI(APITestCase):
|
||||
+ f"?email_stage={stage.pk}"
|
||||
)
|
||||
self.assertEqual(response.status_code, 204)
|
||||
|
||||
def test_service_account(self):
|
||||
"""Service account creation"""
|
||||
self.client.force_login(self.admin)
|
||||
response = self.client.post(reverse("authentik_api:user-service-account"))
|
||||
self.assertEqual(response.status_code, 400)
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:user-service-account"),
|
||||
data={
|
||||
"name": "test-sa",
|
||||
"create_group": True,
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTrue(User.objects.filter(username="test-sa").exists())
|
||||
|
||||
def test_service_account_invalid(self):
|
||||
"""Service account creation (twice with same name, expect error)"""
|
||||
self.client.force_login(self.admin)
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:user-service-account"),
|
||||
data={
|
||||
"name": "test-sa",
|
||||
"create_group": True,
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTrue(User.objects.filter(username="test-sa").exists())
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:user-service-account"),
|
||||
data={
|
||||
"name": "test-sa",
|
||||
"create_group": True,
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
@ -10,7 +10,7 @@ from authentik.crypto.api import CertificateKeyPairSerializer
|
||||
from authentik.crypto.builder import CertificateBuilder
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.flows.models import Flow
|
||||
from authentik.providers.oauth2.generators import generate_client_secret
|
||||
from authentik.lib.generators import generate_key
|
||||
from authentik.providers.oauth2.models import OAuth2Provider
|
||||
|
||||
|
||||
@ -103,7 +103,7 @@ class TestCrypto(TestCase):
|
||||
provider = OAuth2Provider.objects.create(
|
||||
name="test",
|
||||
client_id="test",
|
||||
client_secret=generate_client_secret(),
|
||||
client_secret=generate_key(),
|
||||
authorization_flow=Flow.objects.first(),
|
||||
redirect_uris="http://localhost",
|
||||
rsa_key=CertificateKeyPair.objects.first(),
|
||||
|
@ -15,6 +15,7 @@ from authentik.flows.planner import PLAN_CONTEXT_SOURCE, FlowPlan
|
||||
from authentik.flows.views import SESSION_KEY_PLAN
|
||||
from authentik.stages.invitation.models import Invitation
|
||||
from authentik.stages.invitation.signals import invitation_used
|
||||
from authentik.stages.password.stage import PLAN_CONTEXT_METHOD, PLAN_CONTEXT_METHOD_ARGS
|
||||
from authentik.stages.user_write.signals import user_write
|
||||
|
||||
|
||||
@ -46,7 +47,13 @@ def on_user_logged_in(sender, request: HttpRequest, user: User, **_):
|
||||
flow_plan: FlowPlan = request.session[SESSION_KEY_PLAN]
|
||||
if PLAN_CONTEXT_SOURCE in flow_plan.context:
|
||||
# Login request came from an external source, save it in the context
|
||||
thread.kwargs["using_source"] = flow_plan.context[PLAN_CONTEXT_SOURCE]
|
||||
thread.kwargs[PLAN_CONTEXT_SOURCE] = flow_plan.context[PLAN_CONTEXT_SOURCE]
|
||||
if PLAN_CONTEXT_METHOD in flow_plan.context:
|
||||
thread.kwargs[PLAN_CONTEXT_METHOD] = flow_plan.context[PLAN_CONTEXT_METHOD]
|
||||
# Save the login method used
|
||||
thread.kwargs[PLAN_CONTEXT_METHOD_ARGS] = flow_plan.context.get(
|
||||
PLAN_CONTEXT_METHOD_ARGS, {}
|
||||
)
|
||||
thread.user = user
|
||||
thread.run()
|
||||
|
||||
|
@ -7,10 +7,10 @@ from django.http.response import HttpResponseBadRequest, JsonResponse
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext as _
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from drf_spectacular.utils import OpenApiResponse, extend_schema, inline_serializer
|
||||
from drf_spectacular.utils import OpenApiResponse, extend_schema
|
||||
from guardian.shortcuts import get_objects_for_user
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.fields import BooleanField, FileField, ReadOnlyField
|
||||
from rest_framework.fields import ReadOnlyField
|
||||
from rest_framework.parsers import MultiPartParser
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
@ -20,7 +20,12 @@ from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.api.decorators import permission_required
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.core.api.utils import CacheSerializer, LinkSerializer
|
||||
from authentik.core.api.utils import (
|
||||
CacheSerializer,
|
||||
FilePathSerializer,
|
||||
FileUploadSerializer,
|
||||
LinkSerializer,
|
||||
)
|
||||
from authentik.flows.exceptions import FlowNonApplicableException
|
||||
from authentik.flows.models import Flow
|
||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner, cache_key
|
||||
@ -147,7 +152,7 @@ class FlowViewSet(UsedByMixin, ModelViewSet):
|
||||
],
|
||||
)
|
||||
@extend_schema(
|
||||
request={"multipart/form-data": inline_serializer("SetIcon", fields={"file": FileField()})},
|
||||
request={"multipart/form-data": FileUploadSerializer},
|
||||
responses={
|
||||
204: OpenApiResponse(description="Successfully imported flow"),
|
||||
400: OpenApiResponse(description="Bad request"),
|
||||
@ -259,13 +264,7 @@ class FlowViewSet(UsedByMixin, ModelViewSet):
|
||||
@permission_required("authentik_flows.change_flow")
|
||||
@extend_schema(
|
||||
request={
|
||||
"multipart/form-data": inline_serializer(
|
||||
"SetIcon",
|
||||
fields={
|
||||
"file": FileField(required=False),
|
||||
"clear": BooleanField(default=False),
|
||||
},
|
||||
)
|
||||
"multipart/form-data": FileUploadSerializer,
|
||||
},
|
||||
responses={
|
||||
200: OpenApiResponse(description="Success"),
|
||||
@ -301,7 +300,7 @@ class FlowViewSet(UsedByMixin, ModelViewSet):
|
||||
|
||||
@permission_required("authentik_core.change_application")
|
||||
@extend_schema(
|
||||
request=inline_serializer("SetIconURL", fields={"url": CharField()}),
|
||||
request=FilePathSerializer,
|
||||
responses={
|
||||
200: OpenApiResponse(description="Success"),
|
||||
400: OpenApiResponse(description="Bad request"),
|
||||
|
@ -11,7 +11,7 @@ class Command(BaseCommand): # pragma: no cover
|
||||
def handle(self, *args, **options):
|
||||
"""Apply all flows in order, abort when one fails to import"""
|
||||
for flow_path in options.get("flows", []):
|
||||
with open(flow_path, "r") as flow_file:
|
||||
with open(flow_path, "r", encoding="utf8") as flow_file:
|
||||
importer = FlowImporter(flow_file.read())
|
||||
valid = importer.validate()
|
||||
if not valid:
|
||||
|
@ -6,7 +6,7 @@ from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||
|
||||
from authentik.flows.models import FlowDesignation
|
||||
from authentik.stages.identification.models import UserFields
|
||||
from authentik.stages.password import BACKEND_DJANGO, BACKEND_LDAP
|
||||
from authentik.stages.password import BACKEND_APP_PASSWORD, BACKEND_INBUILT, BACKEND_LDAP
|
||||
|
||||
|
||||
def create_default_authentication_flow(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||
@ -26,7 +26,7 @@ def create_default_authentication_flow(apps: Apps, schema_editor: BaseDatabaseSc
|
||||
|
||||
password_stage, _ = PasswordStage.objects.using(db_alias).update_or_create(
|
||||
name="default-authentication-password",
|
||||
defaults={"backends": [BACKEND_DJANGO, BACKEND_LDAP]},
|
||||
defaults={"backends": [BACKEND_INBUILT, BACKEND_LDAP, BACKEND_APP_PASSWORD]},
|
||||
)
|
||||
|
||||
login_stage, _ = UserLoginStage.objects.using(db_alias).update_or_create(
|
||||
|
@ -3,7 +3,6 @@ from unittest.mock import MagicMock, Mock, PropertyMock, patch
|
||||
|
||||
from django.contrib.sessions.middleware import SessionMiddleware
|
||||
from django.core.cache import cache
|
||||
from django.http import HttpRequest
|
||||
from django.test import RequestFactory, TestCase
|
||||
from django.urls import reverse
|
||||
from guardian.shortcuts import get_anonymous_user
|
||||
@ -13,6 +12,7 @@ from authentik.flows.exceptions import EmptyFlowException, FlowNonApplicableExce
|
||||
from authentik.flows.markers import ReevaluateMarker, StageMarker
|
||||
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding
|
||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner, cache_key
|
||||
from authentik.lib.tests.utils import dummy_get_response
|
||||
from authentik.policies.dummy.models import DummyPolicy
|
||||
from authentik.policies.models import PolicyBinding
|
||||
from authentik.policies.types import PolicyResult
|
||||
@ -24,11 +24,6 @@ CACHE_MOCK = Mock(wraps=cache)
|
||||
POLICY_RETURN_TRUE = MagicMock(return_value=PolicyResult(True))
|
||||
|
||||
|
||||
def dummy_get_response(request: HttpRequest): # pragma: no cover
|
||||
"""Dummy get_response for SessionMiddleware"""
|
||||
return None
|
||||
|
||||
|
||||
class TestFlowPlanner(TestCase):
|
||||
"""Test planner logic"""
|
||||
|
||||
|
@ -7,9 +7,9 @@ from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding
|
||||
from authentik.flows.transfer.common import DataclassEncoder
|
||||
from authentik.flows.transfer.exporter import FlowExporter
|
||||
from authentik.flows.transfer.importer import FlowImporter, transaction_rollback
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.policies.expression.models import ExpressionPolicy
|
||||
from authentik.policies.models import PolicyBinding
|
||||
from authentik.providers.oauth2.generators import generate_client_id
|
||||
from authentik.stages.prompt.models import FieldTypes, Prompt, PromptStage
|
||||
from authentik.stages.user_login.models import UserLoginStage
|
||||
|
||||
@ -31,15 +31,15 @@ class TestFlowTransfer(TransactionTestCase):
|
||||
|
||||
def test_export_validate_import(self):
|
||||
"""Test export and validate it"""
|
||||
flow_slug = generate_client_id()
|
||||
flow_slug = generate_id()
|
||||
with transaction_rollback():
|
||||
login_stage = UserLoginStage.objects.create(name=generate_client_id())
|
||||
login_stage = UserLoginStage.objects.create(name=generate_id())
|
||||
|
||||
flow = Flow.objects.create(
|
||||
slug=flow_slug,
|
||||
designation=FlowDesignation.AUTHENTICATION,
|
||||
name=generate_client_id(),
|
||||
title=generate_client_id(),
|
||||
name=generate_id(),
|
||||
title=generate_id(),
|
||||
)
|
||||
FlowStageBinding.objects.update_or_create(
|
||||
target=flow,
|
||||
@ -60,18 +60,18 @@ class TestFlowTransfer(TransactionTestCase):
|
||||
|
||||
def test_export_validate_import_policies(self):
|
||||
"""Test export and validate it"""
|
||||
flow_slug = generate_client_id()
|
||||
stage_name = generate_client_id()
|
||||
flow_slug = generate_id()
|
||||
stage_name = generate_id()
|
||||
with transaction_rollback():
|
||||
flow_policy = ExpressionPolicy.objects.create(
|
||||
name=generate_client_id(),
|
||||
name=generate_id(),
|
||||
expression="return True",
|
||||
)
|
||||
flow = Flow.objects.create(
|
||||
slug=flow_slug,
|
||||
designation=FlowDesignation.AUTHENTICATION,
|
||||
name=generate_client_id(),
|
||||
title=generate_client_id(),
|
||||
name=generate_id(),
|
||||
title=generate_id(),
|
||||
)
|
||||
PolicyBinding.objects.create(policy=flow_policy, target=flow, order=0)
|
||||
|
||||
@ -111,15 +111,15 @@ class TestFlowTransfer(TransactionTestCase):
|
||||
)
|
||||
|
||||
# Stages
|
||||
first_stage = PromptStage.objects.create(name=generate_client_id())
|
||||
first_stage = PromptStage.objects.create(name=generate_id())
|
||||
first_stage.fields.set([username_prompt, password, password_repeat])
|
||||
first_stage.save()
|
||||
|
||||
flow = Flow.objects.create(
|
||||
name=generate_client_id(),
|
||||
slug=generate_client_id(),
|
||||
name=generate_id(),
|
||||
slug=generate_id(),
|
||||
designation=FlowDesignation.ENROLLMENT,
|
||||
title=generate_client_id(),
|
||||
title=generate_id(),
|
||||
)
|
||||
|
||||
FlowStageBinding.objects.create(target=flow, stage=first_stage, order=0)
|
||||
|
@ -16,7 +16,7 @@ def pbflow_tester(file_name: str) -> Callable:
|
||||
"""This is used instead of subTest for better visibility"""
|
||||
|
||||
def tester(self: TestTransferDocs):
|
||||
with open(file_name, "r") as flow_json:
|
||||
with open(file_name, "r", encoding="utf8") as flow_json:
|
||||
importer = FlowImporter(flow_json.read())
|
||||
self.assertTrue(importer.validate())
|
||||
self.assertTrue(importer.apply())
|
||||
|
@ -25,6 +25,8 @@ def get_attrs(obj: SerializerModel) -> dict[str, Any]:
|
||||
"component",
|
||||
"flow_set",
|
||||
"promptstage_set",
|
||||
"policybindingmodel_ptr_id",
|
||||
"export_url",
|
||||
)
|
||||
for to_remove_name in to_remove:
|
||||
if to_remove_name in data:
|
||||
|
@ -79,7 +79,7 @@ class ConfigLoader:
|
||||
value = os.getenv(url.netloc, url.query)
|
||||
if url.scheme == "file":
|
||||
try:
|
||||
with open(url.path, "r") as _file:
|
||||
with open(url.path, "r", encoding="utf8") as _file:
|
||||
value = _file.read()
|
||||
except OSError:
|
||||
self._log("error", f"Failed to read config value from {url.path}")
|
||||
@ -89,7 +89,7 @@ class ConfigLoader:
|
||||
def update_from_file(self, path: str):
|
||||
"""Update config from file contents"""
|
||||
try:
|
||||
with open(path) as file:
|
||||
with open(path, encoding="utf8") as file:
|
||||
try:
|
||||
self.update(self.__config, yaml.safe_load(file))
|
||||
self._log("debug", "Loaded config", file=path)
|
||||
|
18
authentik/lib/generators.py
Normal file
18
authentik/lib/generators.py
Normal file
@ -0,0 +1,18 @@
|
||||
"""ID/Secret Generators"""
|
||||
import string
|
||||
from random import SystemRandom
|
||||
|
||||
|
||||
def generate_id(length=40):
|
||||
"""Generate a random client ID"""
|
||||
rand = SystemRandom()
|
||||
return "".join(rand.choice(string.ascii_letters + string.digits) for x in range(length))
|
||||
|
||||
|
||||
def generate_key(length=128):
|
||||
"""Generate a suitable client secret"""
|
||||
rand = SystemRandom()
|
||||
return "".join(
|
||||
rand.choice(string.ascii_letters + string.digits + string.punctuation)
|
||||
for x in range(length)
|
||||
)
|
27
authentik/lib/tests/utils.py
Normal file
27
authentik/lib/tests/utils.py
Normal file
@ -0,0 +1,27 @@
|
||||
"""Test utils"""
|
||||
from django.contrib.messages.middleware import MessageMiddleware
|
||||
from django.contrib.sessions.middleware import SessionMiddleware
|
||||
from django.http import HttpRequest
|
||||
from django.test.client import RequestFactory
|
||||
from guardian.utils import get_anonymous_user
|
||||
|
||||
|
||||
def dummy_get_response(request: HttpRequest): # pragma: no cover
|
||||
"""Dummy get_response for SessionMiddleware"""
|
||||
return None
|
||||
|
||||
|
||||
def get_request(*args, user=None, **kwargs):
|
||||
"""Get a request with usable session"""
|
||||
request = RequestFactory().get(*args, **kwargs)
|
||||
if user:
|
||||
request.user = user
|
||||
else:
|
||||
request.user = get_anonymous_user()
|
||||
middleware = SessionMiddleware(dummy_get_response)
|
||||
middleware.process_request(request)
|
||||
request.session.save()
|
||||
middleware = MessageMiddleware(dummy_get_response)
|
||||
middleware.process_request(request)
|
||||
request.session.save()
|
||||
return request
|
@ -102,9 +102,11 @@ class DockerController(BaseController):
|
||||
)
|
||||
|
||||
# pylint: disable=too-many-return-statements
|
||||
def up(self):
|
||||
def up(self, depth=1):
|
||||
if self.outpost.managed == MANAGED_OUTPOST:
|
||||
return None
|
||||
if depth >= 10:
|
||||
raise ControllerException("Giving up since we exceeded recursion limit.")
|
||||
try:
|
||||
container, has_been_created = self._get_container()
|
||||
if has_been_created:
|
||||
@ -120,17 +122,17 @@ class DockerController(BaseController):
|
||||
should=self.get_container_image(),
|
||||
)
|
||||
self.down()
|
||||
return self.up()
|
||||
return self.up(depth + 1)
|
||||
# Check container's ports
|
||||
if self._comp_ports(container):
|
||||
self.logger.info("Container has mis-matched ports, re-creating...")
|
||||
self.down()
|
||||
return self.up()
|
||||
return self.up(depth + 1)
|
||||
# Check that container values match our values
|
||||
if self._comp_env(container):
|
||||
self.logger.info("Container has outdated config, re-creating...")
|
||||
self.down()
|
||||
return self.up()
|
||||
return self.up(depth + 1)
|
||||
if (
|
||||
container.attrs.get("HostConfig", {})
|
||||
.get("RestartPolicy", {})
|
||||
@ -140,7 +142,7 @@ class DockerController(BaseController):
|
||||
):
|
||||
self.logger.info("Container has mis-matched restart policy, re-creating...")
|
||||
self.down()
|
||||
return self.up()
|
||||
return self.up(depth + 1)
|
||||
# Check that container is healthy
|
||||
if (
|
||||
container.status == "running"
|
||||
|
@ -1,11 +1,13 @@
|
||||
"""k8s utils"""
|
||||
from pathlib import Path
|
||||
|
||||
from kubernetes.config.incluster_config import SERVICE_TOKEN_FILENAME
|
||||
|
||||
|
||||
def get_namespace() -> str:
|
||||
"""Get the namespace if we're running in a pod, otherwise default to default"""
|
||||
path = Path("/var/run/secrets/kubernetes.io/serviceaccount/namespace")
|
||||
path = Path(SERVICE_TOKEN_FILENAME.replace("token", "namespace"))
|
||||
if path.exists():
|
||||
with open(path, "r") as _namespace_file:
|
||||
with open(path, "r", encoding="utf8") as _namespace_file:
|
||||
return _namespace_file.read()
|
||||
return "default"
|
||||
|
@ -25,7 +25,7 @@ class DockerInlineTLS:
|
||||
def write_file(self, name: str, contents: str) -> str:
|
||||
"""Wrapper for mkstemp that uses fdopen"""
|
||||
path = Path(gettempdir(), name)
|
||||
with open(path, "w") as _file:
|
||||
with open(path, "w", encoding="utf8") as _file:
|
||||
_file.write(contents)
|
||||
return str(path)
|
||||
|
||||
|
0
authentik/outposts/management/__init__.py
Normal file
0
authentik/outposts/management/__init__.py
Normal file
0
authentik/outposts/management/commands/__init__.py
Normal file
0
authentik/outposts/management/commands/__init__.py
Normal file
15
authentik/outposts/management/commands/repair_permissions.py
Normal file
15
authentik/outposts/management/commands/repair_permissions.py
Normal file
@ -0,0 +1,15 @@
|
||||
"""Repair missing permissions"""
|
||||
from django.apps import apps
|
||||
from django.contrib.auth.management import create_permissions
|
||||
from django.core.management.base import BaseCommand, no_translations
|
||||
|
||||
|
||||
class Command(BaseCommand): # pragma: no cover
|
||||
"""Repair missing permissions"""
|
||||
|
||||
@no_translations
|
||||
def handle(self, *args, **options):
|
||||
"""Check permissions for all apps"""
|
||||
for app in apps.get_app_configs():
|
||||
self.stdout.write(f"Checking app {app.name} ({app.label})\n")
|
||||
create_permissions(app, verbosity=0)
|
@ -37,9 +37,11 @@ from authentik.core.models import (
|
||||
User,
|
||||
)
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.lib.config import CONFIG
|
||||
from authentik.lib.models import InheritanceForeignKey
|
||||
from authentik.lib.sentry import SentryIgnoredException
|
||||
from authentik.lib.utils.errors import exception_to_string
|
||||
from authentik.managed.models import ManagedModel
|
||||
from authentik.outposts.controllers.k8s.utils import get_namespace
|
||||
from authentik.outposts.docker_tls import DockerInlineTLS
|
||||
@ -358,7 +360,24 @@ class Outpost(ManagedModel):
|
||||
code_name = (
|
||||
f"{model_or_perm._meta.app_label}." f"view_{model_or_perm._meta.model_name}"
|
||||
)
|
||||
assign_perm(code_name, user, model_or_perm)
|
||||
try:
|
||||
assign_perm(code_name, user, model_or_perm)
|
||||
except Permission.DoesNotExist as exc:
|
||||
LOGGER.warning(
|
||||
"permission doesn't exist",
|
||||
code_name=code_name,
|
||||
user=user,
|
||||
model=model_or_perm,
|
||||
)
|
||||
Event.new(
|
||||
action=EventAction.SYSTEM_EXCEPTION,
|
||||
message=(
|
||||
"While setting the permissions for the service-account, a "
|
||||
"permission was not found: Check "
|
||||
"https://goauthentik.io/docs/troubleshooting/missing_permission"
|
||||
)
|
||||
+ exception_to_string(exc),
|
||||
).set_user(user).save()
|
||||
else:
|
||||
app_label, perm = model_or_perm.split(".")
|
||||
permission = Permission.objects.filter(
|
||||
|
@ -227,7 +227,7 @@ def outpost_local_connection():
|
||||
kubeconfig_local_name = f"k8s-{gethostname()}"
|
||||
if not KubernetesServiceConnection.objects.filter(name=kubeconfig_local_name).exists():
|
||||
LOGGER.debug("Creating kubeconfig Service Connection")
|
||||
with open(kubeconfig_path, "r") as _kubeconfig:
|
||||
with open(kubeconfig_path, "r", encoding="utf8") as _kubeconfig:
|
||||
KubernetesServiceConnection.objects.create(
|
||||
name=kubeconfig_local_name,
|
||||
kubeconfig=yaml.safe_load(_kubeconfig),
|
||||
|
@ -2,9 +2,9 @@
|
||||
from django.test import TestCase
|
||||
from guardian.shortcuts import get_anonymous_user
|
||||
|
||||
from authentik.lib.generators import generate_key
|
||||
from authentik.policies.hibp.models import HaveIBeenPwendPolicy
|
||||
from authentik.policies.types import PolicyRequest, PolicyResult
|
||||
from authentik.providers.oauth2.generators import generate_client_secret
|
||||
|
||||
|
||||
class TestHIBPPolicy(TestCase):
|
||||
@ -37,7 +37,7 @@ class TestHIBPPolicy(TestCase):
|
||||
name="test_true",
|
||||
)
|
||||
request = PolicyRequest(get_anonymous_user())
|
||||
request.context["password"] = generate_client_secret()
|
||||
request.context["password"] = generate_key()
|
||||
result: PolicyResult = policy.passes(request)
|
||||
self.assertTrue(result.passing)
|
||||
self.assertEqual(result.messages, tuple())
|
||||
|
@ -3,7 +3,7 @@ from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from drf_spectacular.utils import OpenApiResponse, extend_schema
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.fields import ReadOnlyField
|
||||
from rest_framework.fields import CharField
|
||||
from rest_framework.generics import get_object_or_404
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
@ -49,12 +49,12 @@ class OAuth2ProviderSerializer(ProviderSerializer):
|
||||
class OAuth2ProviderSetupURLs(PassiveSerializer):
|
||||
"""OAuth2 Provider Metadata serializer"""
|
||||
|
||||
issuer = ReadOnlyField()
|
||||
authorize = ReadOnlyField()
|
||||
token = ReadOnlyField()
|
||||
user_info = ReadOnlyField()
|
||||
provider_info = ReadOnlyField()
|
||||
logout = ReadOnlyField()
|
||||
issuer = CharField(read_only=True)
|
||||
authorize = CharField(read_only=True)
|
||||
token = CharField(read_only=True)
|
||||
user_info = CharField(read_only=True)
|
||||
provider_info = CharField(read_only=True)
|
||||
logout = CharField(read_only=True)
|
||||
|
||||
|
||||
class OAuth2ProviderViewSet(UsedByMixin, ModelViewSet):
|
||||
|
@ -1,15 +0,0 @@
|
||||
"""OAuth2 Client ID/Secret Generators"""
|
||||
import string
|
||||
from random import SystemRandom
|
||||
|
||||
|
||||
def generate_client_id():
|
||||
"""Generate a random client ID"""
|
||||
rand = SystemRandom()
|
||||
return "".join(rand.choice(string.ascii_letters + string.digits) for x in range(40))
|
||||
|
||||
|
||||
def generate_client_secret():
|
||||
"""Generate a suitable client secret"""
|
||||
rand = SystemRandom()
|
||||
return "".join(rand.choice(string.ascii_letters + string.digits) for x in range(128))
|
@ -7,8 +7,8 @@ from django.db import migrations, models
|
||||
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||
|
||||
import authentik.core.models
|
||||
import authentik.lib.generators
|
||||
import authentik.lib.utils.time
|
||||
import authentik.providers.oauth2.generators
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
@ -55,7 +55,7 @@ class Migration(migrations.Migration):
|
||||
(
|
||||
"client_id",
|
||||
models.CharField(
|
||||
default=authentik.providers.oauth2.generators.generate_client_id,
|
||||
default=authentik.lib.generators.generate_id,
|
||||
max_length=255,
|
||||
unique=True,
|
||||
verbose_name="Client ID",
|
||||
@ -65,7 +65,7 @@ class Migration(migrations.Migration):
|
||||
"client_secret",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
default=authentik.providers.oauth2.generators.generate_client_secret,
|
||||
default=authentik.lib.generators.generate_key,
|
||||
max_length=255,
|
||||
verbose_name="Client Secret",
|
||||
),
|
||||
|
@ -22,10 +22,10 @@ from authentik.core.models import ExpiringModel, PropertyMapping, Provider, User
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.events.utils import get_user
|
||||
from authentik.lib.generators import generate_id, generate_key
|
||||
from authentik.lib.utils.time import timedelta_from_string, timedelta_string_validator
|
||||
from authentik.providers.oauth2.apps import AuthentikProviderOAuth2Config
|
||||
from authentik.providers.oauth2.constants import ACR_AUTHENTIK_DEFAULT
|
||||
from authentik.providers.oauth2.generators import generate_client_id, generate_client_secret
|
||||
|
||||
|
||||
class ClientTypes(models.TextChoices):
|
||||
@ -138,13 +138,13 @@ class OAuth2Provider(Provider):
|
||||
max_length=255,
|
||||
unique=True,
|
||||
verbose_name=_("Client ID"),
|
||||
default=generate_client_id,
|
||||
default=generate_id,
|
||||
)
|
||||
client_secret = models.CharField(
|
||||
max_length=255,
|
||||
blank=True,
|
||||
verbose_name=_("Client Secret"),
|
||||
default=generate_client_secret,
|
||||
default=generate_key,
|
||||
)
|
||||
jwt_alg = models.CharField(
|
||||
max_length=10,
|
||||
|
@ -7,8 +7,8 @@ from authentik.core.models import Application, User
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.flows.challenge import ChallengeTypes
|
||||
from authentik.flows.models import Flow
|
||||
from authentik.lib.generators import generate_id, generate_key
|
||||
from authentik.providers.oauth2.errors import AuthorizeError, ClientIdError, RedirectUriError
|
||||
from authentik.providers.oauth2.generators import generate_client_id, generate_client_secret
|
||||
from authentik.providers.oauth2.models import (
|
||||
AuthorizationCode,
|
||||
GrantTypes,
|
||||
@ -183,7 +183,7 @@ class TestAuthorize(OAuthTestCase):
|
||||
redirect_uris="foo://localhost",
|
||||
)
|
||||
Application.objects.create(name="app", slug="app", provider=provider)
|
||||
state = generate_client_id()
|
||||
state = generate_id()
|
||||
user = User.objects.get(username="akadmin")
|
||||
self.client.force_login(user)
|
||||
# Step 1, initiate params and get redirect to flow
|
||||
@ -215,13 +215,13 @@ class TestAuthorize(OAuthTestCase):
|
||||
provider = OAuth2Provider.objects.create(
|
||||
name="test",
|
||||
client_id="test",
|
||||
client_secret=generate_client_secret(),
|
||||
client_secret=generate_key(),
|
||||
authorization_flow=flow,
|
||||
redirect_uris="http://localhost",
|
||||
rsa_key=CertificateKeyPair.objects.first(),
|
||||
)
|
||||
Application.objects.create(name="app", slug="app", provider=provider)
|
||||
state = generate_client_id()
|
||||
state = generate_id()
|
||||
user = User.objects.get(username="akadmin")
|
||||
self.client.force_login(user)
|
||||
# Step 1, initiate params and get redirect to flow
|
||||
|
@ -9,12 +9,12 @@ from authentik.core.models import Application, User
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.flows.models import Flow
|
||||
from authentik.lib.generators import generate_id, generate_key
|
||||
from authentik.providers.oauth2.constants import (
|
||||
GRANT_TYPE_AUTHORIZATION_CODE,
|
||||
GRANT_TYPE_REFRESH_TOKEN,
|
||||
)
|
||||
from authentik.providers.oauth2.errors import TokenError
|
||||
from authentik.providers.oauth2.generators import generate_client_id, generate_client_secret
|
||||
from authentik.providers.oauth2.models import AuthorizationCode, OAuth2Provider, RefreshToken
|
||||
from authentik.providers.oauth2.tests.utils import OAuthTestCase
|
||||
from authentik.providers.oauth2.views.token import TokenParams
|
||||
@ -32,8 +32,8 @@ class TestToken(OAuthTestCase):
|
||||
"""test request param"""
|
||||
provider = OAuth2Provider.objects.create(
|
||||
name="test",
|
||||
client_id=generate_client_id(),
|
||||
client_secret=generate_client_secret(),
|
||||
client_id=generate_id(),
|
||||
client_secret=generate_key(),
|
||||
authorization_flow=Flow.objects.first(),
|
||||
redirect_uris="http://testserver",
|
||||
rsa_key=CertificateKeyPair.objects.first(),
|
||||
@ -53,14 +53,14 @@ class TestToken(OAuthTestCase):
|
||||
params = TokenParams.parse(request, provider, provider.client_id, provider.client_secret)
|
||||
self.assertEqual(params.provider, provider)
|
||||
with self.assertRaises(TokenError):
|
||||
TokenParams.parse(request, provider, provider.client_id, generate_client_secret())
|
||||
TokenParams.parse(request, provider, provider.client_id, generate_key())
|
||||
|
||||
def test_request_auth_code_invalid(self):
|
||||
"""test request param"""
|
||||
provider = OAuth2Provider.objects.create(
|
||||
name="test",
|
||||
client_id=generate_client_id(),
|
||||
client_secret=generate_client_secret(),
|
||||
client_id=generate_id(),
|
||||
client_secret=generate_key(),
|
||||
authorization_flow=Flow.objects.first(),
|
||||
redirect_uris="http://testserver",
|
||||
rsa_key=CertificateKeyPair.objects.first(),
|
||||
@ -82,8 +82,8 @@ class TestToken(OAuthTestCase):
|
||||
"""test request param"""
|
||||
provider = OAuth2Provider.objects.create(
|
||||
name="test",
|
||||
client_id=generate_client_id(),
|
||||
client_secret=generate_client_secret(),
|
||||
client_id=generate_id(),
|
||||
client_secret=generate_key(),
|
||||
authorization_flow=Flow.objects.first(),
|
||||
redirect_uris="http://local.invalid",
|
||||
rsa_key=CertificateKeyPair.objects.first(),
|
||||
@ -93,7 +93,7 @@ class TestToken(OAuthTestCase):
|
||||
token: RefreshToken = RefreshToken.objects.create(
|
||||
provider=provider,
|
||||
user=user,
|
||||
refresh_token=generate_client_id(),
|
||||
refresh_token=generate_id(),
|
||||
)
|
||||
request = self.factory.post(
|
||||
"/",
|
||||
@ -111,8 +111,8 @@ class TestToken(OAuthTestCase):
|
||||
"""test request param"""
|
||||
provider = OAuth2Provider.objects.create(
|
||||
name="test",
|
||||
client_id=generate_client_id(),
|
||||
client_secret=generate_client_secret(),
|
||||
client_id=generate_id(),
|
||||
client_secret=generate_key(),
|
||||
authorization_flow=Flow.objects.first(),
|
||||
redirect_uris="http://local.invalid",
|
||||
rsa_key=CertificateKeyPair.objects.first(),
|
||||
@ -153,8 +153,8 @@ class TestToken(OAuthTestCase):
|
||||
"""test request param"""
|
||||
provider = OAuth2Provider.objects.create(
|
||||
name="test",
|
||||
client_id=generate_client_id(),
|
||||
client_secret=generate_client_secret(),
|
||||
client_id=generate_id(),
|
||||
client_secret=generate_key(),
|
||||
authorization_flow=Flow.objects.first(),
|
||||
redirect_uris="http://local.invalid",
|
||||
rsa_key=CertificateKeyPair.objects.first(),
|
||||
@ -167,7 +167,7 @@ class TestToken(OAuthTestCase):
|
||||
token: RefreshToken = RefreshToken.objects.create(
|
||||
provider=provider,
|
||||
user=user,
|
||||
refresh_token=generate_client_id(),
|
||||
refresh_token=generate_id(),
|
||||
)
|
||||
response = self.client.post(
|
||||
reverse("authentik_providers_oauth2:token"),
|
||||
@ -202,8 +202,8 @@ class TestToken(OAuthTestCase):
|
||||
"""test request param"""
|
||||
provider = OAuth2Provider.objects.create(
|
||||
name="test",
|
||||
client_id=generate_client_id(),
|
||||
client_secret=generate_client_secret(),
|
||||
client_id=generate_id(),
|
||||
client_secret=generate_key(),
|
||||
authorization_flow=Flow.objects.first(),
|
||||
redirect_uris="http://local.invalid",
|
||||
rsa_key=CertificateKeyPair.objects.first(),
|
||||
@ -213,7 +213,7 @@ class TestToken(OAuthTestCase):
|
||||
token: RefreshToken = RefreshToken.objects.create(
|
||||
provider=provider,
|
||||
user=user,
|
||||
refresh_token=generate_client_id(),
|
||||
refresh_token=generate_id(),
|
||||
)
|
||||
response = self.client.post(
|
||||
reverse("authentik_providers_oauth2:token"),
|
||||
@ -247,8 +247,8 @@ class TestToken(OAuthTestCase):
|
||||
"""test request param"""
|
||||
provider = OAuth2Provider.objects.create(
|
||||
name="test",
|
||||
client_id=generate_client_id(),
|
||||
client_secret=generate_client_secret(),
|
||||
client_id=generate_id(),
|
||||
client_secret=generate_key(),
|
||||
authorization_flow=Flow.objects.first(),
|
||||
redirect_uris="http://testserver",
|
||||
rsa_key=CertificateKeyPair.objects.first(),
|
||||
@ -261,7 +261,7 @@ class TestToken(OAuthTestCase):
|
||||
token: RefreshToken = RefreshToken.objects.create(
|
||||
provider=provider,
|
||||
user=user,
|
||||
refresh_token=generate_client_id(),
|
||||
refresh_token=generate_id(),
|
||||
)
|
||||
# Create initial refresh token
|
||||
response = self.client.post(
|
||||
|
@ -9,8 +9,8 @@ from authentik.core.models import Application, User
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.flows.models import Flow
|
||||
from authentik.lib.generators import generate_id, generate_key
|
||||
from authentik.managed.manager import ObjectManager
|
||||
from authentik.providers.oauth2.generators import generate_client_id, generate_client_secret
|
||||
from authentik.providers.oauth2.models import IDToken, OAuth2Provider, RefreshToken, ScopeMapping
|
||||
from authentik.providers.oauth2.tests.utils import OAuthTestCase
|
||||
|
||||
@ -24,8 +24,8 @@ class TestUserinfo(OAuthTestCase):
|
||||
self.app = Application.objects.create(name="test", slug="test")
|
||||
self.provider: OAuth2Provider = OAuth2Provider.objects.create(
|
||||
name="test",
|
||||
client_id=generate_client_id(),
|
||||
client_secret=generate_client_secret(),
|
||||
client_id=generate_id(),
|
||||
client_secret=generate_key(),
|
||||
authorization_flow=Flow.objects.first(),
|
||||
redirect_uris="",
|
||||
rsa_key=CertificateKeyPair.objects.first(),
|
||||
@ -38,8 +38,8 @@ class TestUserinfo(OAuthTestCase):
|
||||
self.token: RefreshToken = RefreshToken.objects.create(
|
||||
provider=self.provider,
|
||||
user=self.user,
|
||||
access_token=generate_client_id(),
|
||||
refresh_token=generate_client_id(),
|
||||
access_token=generate_id(),
|
||||
refresh_token=generate_id(),
|
||||
_scope="openid user profile",
|
||||
_id_token=json.dumps(
|
||||
asdict(
|
||||
|
@ -103,8 +103,8 @@ def extract_client_auth(request: HttpRequest) -> tuple[str, str]:
|
||||
if re.compile(r"^Basic\s{1}.+$").match(auth_header):
|
||||
b64_user_pass = auth_header.split()[1]
|
||||
try:
|
||||
user_pass = b64decode(b64_user_pass).decode("utf-8").split(":")
|
||||
client_id, client_secret = user_pass
|
||||
user_pass = b64decode(b64_user_pass).decode("utf-8").partition(":")
|
||||
client_id, _, client_secret = user_pass
|
||||
except (ValueError, Error):
|
||||
client_id = client_secret = "" # nosec
|
||||
else:
|
||||
|
@ -11,7 +11,7 @@ from django_filters.filterset import FilterSet
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.fields import CharField, FileField, ReadOnlyField, SerializerMethodField
|
||||
from rest_framework.fields import CharField, FileField, SerializerMethodField
|
||||
from rest_framework.parsers import MultiPartParser
|
||||
from rest_framework.permissions import AllowAny
|
||||
from rest_framework.relations import SlugRelatedField
|
||||
@ -70,8 +70,8 @@ class SAMLProviderSerializer(ProviderSerializer):
|
||||
class SAMLMetadataSerializer(PassiveSerializer):
|
||||
"""SAML Provider Metadata serializer"""
|
||||
|
||||
metadata = ReadOnlyField()
|
||||
download_url = ReadOnlyField(required=False)
|
||||
metadata = CharField(read_only=True)
|
||||
download_url = CharField(read_only=True, required=False)
|
||||
|
||||
|
||||
class SAMLProviderImportSerializer(PassiveSerializer):
|
||||
|
@ -1,16 +1,14 @@
|
||||
"""Test AuthN Request generator and parser"""
|
||||
from base64 import b64encode
|
||||
|
||||
from django.contrib.sessions.middleware import SessionMiddleware
|
||||
from django.http.request import QueryDict
|
||||
from django.test import RequestFactory, TestCase
|
||||
from guardian.utils import get_anonymous_user
|
||||
|
||||
from authentik.core.models import User
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.flows.models import Flow
|
||||
from authentik.flows.tests.test_planner import dummy_get_response
|
||||
from authentik.lib.tests.utils import get_request
|
||||
from authentik.managed.manager import ObjectManager
|
||||
from authentik.providers.saml.models import SAMLPropertyMapping, SAMLProvider
|
||||
from authentik.providers.saml.processors.assertion import AssertionProcessor
|
||||
@ -99,11 +97,7 @@ class TestAuthNRequest(TestCase):
|
||||
|
||||
def test_signed_valid(self):
|
||||
"""Test generated AuthNRequest with valid signature"""
|
||||
http_request = self.factory.get("/")
|
||||
|
||||
middleware = SessionMiddleware(dummy_get_response)
|
||||
middleware.process_request(http_request)
|
||||
http_request.session.save()
|
||||
http_request = get_request("/")
|
||||
|
||||
# First create an AuthNRequest
|
||||
request_proc = RequestProcessor(self.source, http_request, "test_state")
|
||||
@ -117,12 +111,7 @@ class TestAuthNRequest(TestCase):
|
||||
|
||||
def test_request_full_signed(self):
|
||||
"""Test full SAML Request/Response flow, fully signed"""
|
||||
http_request = self.factory.get("/")
|
||||
http_request.user = get_anonymous_user()
|
||||
|
||||
middleware = SessionMiddleware(dummy_get_response)
|
||||
middleware.process_request(http_request)
|
||||
http_request.session.save()
|
||||
http_request = get_request("/")
|
||||
|
||||
# First create an AuthNRequest
|
||||
request_proc = RequestProcessor(self.source, http_request, "test_state")
|
||||
@ -145,12 +134,7 @@ class TestAuthNRequest(TestCase):
|
||||
|
||||
def test_request_id_invalid(self):
|
||||
"""Test generated AuthNRequest with invalid request ID"""
|
||||
http_request = self.factory.get("/")
|
||||
http_request.user = get_anonymous_user()
|
||||
|
||||
middleware = SessionMiddleware(dummy_get_response)
|
||||
middleware.process_request(http_request)
|
||||
http_request.session.save()
|
||||
http_request = get_request("/")
|
||||
|
||||
# First create an AuthNRequest
|
||||
request_proc = RequestProcessor(self.source, http_request, "test_state")
|
||||
@ -179,11 +163,7 @@ class TestAuthNRequest(TestCase):
|
||||
|
||||
def test_signed_valid_detached(self):
|
||||
"""Test generated AuthNRequest with valid signature (detached)"""
|
||||
http_request = self.factory.get("/")
|
||||
|
||||
middleware = SessionMiddleware(dummy_get_response)
|
||||
middleware.process_request(http_request)
|
||||
http_request.session.save()
|
||||
http_request = get_request("/")
|
||||
|
||||
# First create an AuthNRequest
|
||||
request_proc = RequestProcessor(self.source, http_request, "test_state")
|
||||
@ -243,12 +223,7 @@ class TestAuthNRequest(TestCase):
|
||||
|
||||
def test_request_attributes(self):
|
||||
"""Test full SAML Request/Response flow, fully signed"""
|
||||
http_request = self.factory.get("/")
|
||||
http_request.user = User.objects.get(username="akadmin")
|
||||
|
||||
middleware = SessionMiddleware(dummy_get_response)
|
||||
middleware.process_request(http_request)
|
||||
http_request.session.save()
|
||||
http_request = get_request("/", user=User.objects.get(username="akadmin"))
|
||||
|
||||
# First create an AuthNRequest
|
||||
request_proc = RequestProcessor(self.source, http_request, "test_state")
|
||||
@ -264,12 +239,7 @@ class TestAuthNRequest(TestCase):
|
||||
|
||||
def test_request_attributes_invalid(self):
|
||||
"""Test full SAML Request/Response flow, fully signed"""
|
||||
http_request = self.factory.get("/")
|
||||
http_request.user = User.objects.get(username="akadmin")
|
||||
|
||||
middleware = SessionMiddleware(dummy_get_response)
|
||||
middleware.process_request(http_request)
|
||||
http_request.session.save()
|
||||
http_request = get_request("/", user=User.objects.get(username="akadmin"))
|
||||
|
||||
# First create an AuthNRequest
|
||||
request_proc = RequestProcessor(self.source, http_request, "test_state")
|
||||
|
@ -1,18 +1,16 @@
|
||||
"""Test Requests and Responses against schema"""
|
||||
from base64 import b64encode
|
||||
|
||||
from django.contrib.sessions.middleware import SessionMiddleware
|
||||
from django.test import RequestFactory, TestCase
|
||||
from guardian.utils import get_anonymous_user
|
||||
from lxml import etree # nosec
|
||||
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.flows.models import Flow
|
||||
from authentik.lib.tests.utils import get_request
|
||||
from authentik.managed.manager import ObjectManager
|
||||
from authentik.providers.saml.models import SAMLPropertyMapping, SAMLProvider
|
||||
from authentik.providers.saml.processors.assertion import AssertionProcessor
|
||||
from authentik.providers.saml.processors.request_parser import AuthNRequestParser
|
||||
from authentik.providers.saml.tests.test_auth_n_request import dummy_get_response
|
||||
from authentik.sources.saml.models import SAMLSource
|
||||
from authentik.sources.saml.processors.request import RequestProcessor
|
||||
|
||||
@ -43,11 +41,7 @@ class TestSchema(TestCase):
|
||||
|
||||
def test_request_schema(self):
|
||||
"""Test generated AuthNRequest against Schema"""
|
||||
http_request = self.factory.get("/")
|
||||
|
||||
middleware = SessionMiddleware(dummy_get_response)
|
||||
middleware.process_request(http_request)
|
||||
http_request.session.save()
|
||||
http_request = get_request("/")
|
||||
|
||||
# First create an AuthNRequest
|
||||
request_proc = RequestProcessor(self.source, http_request, "test_state")
|
||||
@ -60,12 +54,7 @@ class TestSchema(TestCase):
|
||||
|
||||
def test_response_schema(self):
|
||||
"""Test generated AuthNRequest against Schema"""
|
||||
http_request = self.factory.get("/")
|
||||
http_request.user = get_anonymous_user()
|
||||
|
||||
middleware = SessionMiddleware(dummy_get_response)
|
||||
middleware.process_request(http_request)
|
||||
http_request.session.save()
|
||||
http_request = get_request("/")
|
||||
|
||||
# First create an AuthNRequest
|
||||
request_proc = RequestProcessor(self.source, http_request, "test_state")
|
||||
|
@ -7,7 +7,7 @@ from django.utils.translation import gettext as _
|
||||
from django.views import View
|
||||
|
||||
from authentik.core.models import Token, TokenIntents
|
||||
from authentik.stages.password import BACKEND_DJANGO
|
||||
from authentik.stages.password import BACKEND_INBUILT
|
||||
|
||||
|
||||
class UseTokenView(View):
|
||||
@ -19,7 +19,7 @@ class UseTokenView(View):
|
||||
if not tokens.exists():
|
||||
raise Http404
|
||||
token = tokens.first()
|
||||
login(request, token.user, backend=BACKEND_DJANGO)
|
||||
login(request, token.user, backend=BACKEND_INBUILT)
|
||||
token.delete()
|
||||
messages.warning(request, _("Used recovery-link to authenticate."))
|
||||
return redirect("authentik_core:if-admin")
|
||||
|
0
authentik/root/asgi/__init__.py
Normal file
0
authentik/root/asgi/__init__.py
Normal file
40
authentik/root/asgi/app.py
Normal file
40
authentik/root/asgi/app.py
Normal file
@ -0,0 +1,40 @@
|
||||
"""
|
||||
ASGI config for authentik project.
|
||||
|
||||
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/3.0/howto/deployment/asgi/
|
||||
"""
|
||||
import django
|
||||
from asgiref.compatibility import guarantee_single_callable
|
||||
from channels.routing import ProtocolTypeRouter, URLRouter
|
||||
from defusedxml import defuse_stdlib
|
||||
from django.core.asgi import get_asgi_application
|
||||
from sentry_sdk.integrations.asgi import SentryAsgiMiddleware
|
||||
|
||||
from authentik.root.asgi.error_handler import ASGIErrorHandler
|
||||
from authentik.root.asgi.logger import ASGILogger
|
||||
|
||||
# DJANGO_SETTINGS_MODULE is set in gunicorn.conf.py
|
||||
|
||||
defuse_stdlib()
|
||||
django.setup()
|
||||
|
||||
# pylint: disable=wrong-import-position
|
||||
from authentik.root import websocket # noqa # isort:skip
|
||||
|
||||
application = ASGIErrorHandler(
|
||||
ASGILogger(
|
||||
guarantee_single_callable(
|
||||
SentryAsgiMiddleware(
|
||||
ProtocolTypeRouter(
|
||||
{
|
||||
"http": get_asgi_application(),
|
||||
"websocket": URLRouter(websocket.websocket_urlpatterns),
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
38
authentik/root/asgi/error_handler.py
Normal file
38
authentik/root/asgi/error_handler.py
Normal file
@ -0,0 +1,38 @@
|
||||
"""ASGI Error handler"""
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.root.asgi.types import ASGIApp, Receive, Scope, Send
|
||||
|
||||
LOGGER = get_logger("authentik.asgi")
|
||||
|
||||
|
||||
class ASGIErrorHandler:
|
||||
"""ASGI Error handler"""
|
||||
|
||||
app: ASGIApp
|
||||
|
||||
def __init__(self, app: ASGIApp):
|
||||
self.app = app
|
||||
|
||||
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
||||
try:
|
||||
return await self.app(scope, receive, send)
|
||||
except Exception as exc: # pylint: disable=broad-except
|
||||
LOGGER.warning("Fatal ASGI exception", exc=exc)
|
||||
return await self.error_handler(scope, send)
|
||||
|
||||
async def error_handler(self, scope: Scope, send: Send) -> None:
|
||||
"""Return a generic error message"""
|
||||
if scope.get("scheme", "http") == "http":
|
||||
return await send(
|
||||
{
|
||||
"type": "http.request",
|
||||
"body": b"Internal server error",
|
||||
"more_body": False,
|
||||
}
|
||||
)
|
||||
return await send(
|
||||
{
|
||||
"type": "websocket.close",
|
||||
}
|
||||
)
|
@ -1,41 +1,10 @@
|
||||
"""
|
||||
ASGI config for authentik project.
|
||||
|
||||
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/3.0/howto/deployment/asgi/
|
||||
"""
|
||||
import typing
|
||||
"""ASGI Logger"""
|
||||
from time import time
|
||||
|
||||
import django
|
||||
from asgiref.compatibility import guarantee_single_callable
|
||||
from channels.routing import ProtocolTypeRouter, URLRouter
|
||||
from defusedxml import defuse_stdlib
|
||||
from django.core.asgi import get_asgi_application
|
||||
from sentry_sdk.integrations.asgi import SentryAsgiMiddleware
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.core.middleware import RESPONSE_HEADER_ID
|
||||
|
||||
# DJANGO_SETTINGS_MODULE is set in gunicorn.conf.py
|
||||
|
||||
defuse_stdlib()
|
||||
django.setup()
|
||||
|
||||
# pylint: disable=wrong-import-position
|
||||
from authentik.root import websocket # noqa # isort:skip
|
||||
|
||||
|
||||
# See https://github.com/encode/starlette/blob/master/starlette/types.py
|
||||
Scope = typing.MutableMapping[str, typing.Any]
|
||||
Message = typing.MutableMapping[str, typing.Any]
|
||||
|
||||
Receive = typing.Callable[[], typing.Awaitable[Message]]
|
||||
Send = typing.Callable[[Message], typing.Awaitable[None]]
|
||||
|
||||
ASGIApp = typing.Callable[[Scope, Receive, Send], typing.Awaitable[None]]
|
||||
from authentik.root.asgi.types import ASGIApp, Message, Receive, Scope, Send
|
||||
|
||||
ASGI_IP_HEADERS = (
|
||||
b"x-forwarded-for",
|
||||
@ -86,7 +55,7 @@ class ASGILogger:
|
||||
# https://code.djangoproject.com/ticket/31508
|
||||
# https://github.com/encode/uvicorn/issues/266
|
||||
return
|
||||
await self.app(scope, receive, send_hooked)
|
||||
return await self.app(scope, receive, send_hooked)
|
||||
|
||||
def _get_ip(self, scope: Scope) -> str:
|
||||
client_ip = None
|
||||
@ -115,17 +84,3 @@ class ASGILogger:
|
||||
runtime=runtime,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
application = ASGILogger(
|
||||
guarantee_single_callable(
|
||||
SentryAsgiMiddleware(
|
||||
ProtocolTypeRouter(
|
||||
{
|
||||
"http": get_asgi_application(),
|
||||
"websocket": URLRouter(websocket.websocket_urlpatterns),
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
11
authentik/root/asgi/types.py
Normal file
11
authentik/root/asgi/types.py
Normal file
@ -0,0 +1,11 @@
|
||||
"""ASGI Types"""
|
||||
import typing
|
||||
|
||||
# See https://github.com/encode/starlette/blob/master/starlette/types.py
|
||||
Scope = typing.MutableMapping[str, typing.Any]
|
||||
Message = typing.MutableMapping[str, typing.Any]
|
||||
|
||||
Receive = typing.Callable[[], typing.Awaitable[Message]]
|
||||
Send = typing.Callable[[Message], typing.Awaitable[None]]
|
||||
|
||||
ASGIApp = typing.Callable[[Scope, Receive, Send], typing.Awaitable[None]]
|
@ -32,6 +32,7 @@ from authentik.core.middleware import structlog_add_request_id
|
||||
from authentik.lib.config import CONFIG
|
||||
from authentik.lib.logging import add_process_id
|
||||
from authentik.lib.sentry import before_send
|
||||
from authentik.stages.password import BACKEND_APP_PASSWORD, BACKEND_INBUILT, BACKEND_LDAP
|
||||
|
||||
|
||||
def j_print(event: str, log_level: str = "info", **kwargs):
|
||||
@ -73,7 +74,9 @@ LANGUAGE_COOKIE_NAME = f"authentik_language{_cookie_suffix}"
|
||||
SESSION_COOKIE_NAME = f"authentik_session{_cookie_suffix}"
|
||||
|
||||
AUTHENTICATION_BACKENDS = [
|
||||
"django.contrib.auth.backends.ModelBackend",
|
||||
BACKEND_INBUILT,
|
||||
BACKEND_APP_PASSWORD,
|
||||
BACKEND_LDAP,
|
||||
"guardian.backends.ObjectPermissionBackend",
|
||||
]
|
||||
|
||||
@ -251,7 +254,7 @@ TEMPLATES = [
|
||||
},
|
||||
]
|
||||
|
||||
ASGI_APPLICATION = "authentik.root.asgi.application"
|
||||
ASGI_APPLICATION = "authentik.root.asgi.app.application"
|
||||
|
||||
CHANNEL_LAYERS = {
|
||||
"default": {
|
||||
|
@ -27,7 +27,10 @@ class LDAPSourceSerializer(SourceSerializer):
|
||||
"""Check that only a single source has password_sync on"""
|
||||
sync_users_password = attrs.get("sync_users_password", True)
|
||||
if sync_users_password:
|
||||
if LDAPSource.objects.filter(sync_users_password=True).exists():
|
||||
sources = LDAPSource.objects.filter(sync_users_password=True)
|
||||
if self.instance:
|
||||
sources = sources.exclude(pk=self.instance.pk)
|
||||
if sources.exists():
|
||||
raise ValidationError(
|
||||
"Only a single LDAP Source with password synchronization is allowed"
|
||||
)
|
||||
|
@ -2,10 +2,10 @@
|
||||
from typing import Optional
|
||||
|
||||
import ldap3
|
||||
from django.contrib.auth.backends import ModelBackend
|
||||
from django.http import HttpRequest
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.core.auth import InbuiltBackend
|
||||
from authentik.core.models import User
|
||||
from authentik.sources.ldap.models import LDAPSource
|
||||
|
||||
@ -13,7 +13,7 @@ LOGGER = get_logger()
|
||||
LDAP_DISTINGUISHED_NAME = "distinguishedName"
|
||||
|
||||
|
||||
class LDAPBackend(ModelBackend):
|
||||
class LDAPBackend(InbuiltBackend):
|
||||
"""Authenticate users against LDAP Server"""
|
||||
|
||||
def authenticate(self, request: HttpRequest, **kwargs):
|
||||
@ -24,6 +24,7 @@ class LDAPBackend(ModelBackend):
|
||||
LOGGER.debug("LDAP Auth attempt", source=source)
|
||||
user = self.auth_user(source, **kwargs)
|
||||
if user:
|
||||
self.set_method("ldap", request, source=source)
|
||||
return user
|
||||
return None
|
||||
|
||||
|
@ -1,10 +1,6 @@
|
||||
"""LDAP Settings"""
|
||||
from celery.schedules import crontab
|
||||
|
||||
AUTHENTICATION_BACKENDS = [
|
||||
"authentik.sources.ldap.auth.LDAPBackend",
|
||||
]
|
||||
|
||||
CELERY_BEAT_SCHEDULE = {
|
||||
"sources_ldap_sync": {
|
||||
"task": "authentik.sources.ldap.tasks.ldap_sync_all",
|
||||
|
@ -1,11 +1,11 @@
|
||||
"""LDAP Source API tests"""
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from authentik.providers.oauth2.generators import generate_client_secret
|
||||
from authentik.lib.generators import generate_key
|
||||
from authentik.sources.ldap.api import LDAPSourceSerializer
|
||||
from authentik.sources.ldap.models import LDAPSource
|
||||
|
||||
LDAP_PASSWORD = generate_client_secret()
|
||||
LDAP_PASSWORD = generate_key()
|
||||
|
||||
|
||||
class LDAPAPITests(APITestCase):
|
||||
|
@ -5,15 +5,15 @@ from django.db.models import Q
|
||||
from django.test import TestCase
|
||||
|
||||
from authentik.core.models import User
|
||||
from authentik.lib.generators import generate_key
|
||||
from authentik.managed.manager import ObjectManager
|
||||
from authentik.providers.oauth2.generators import generate_client_secret
|
||||
from authentik.sources.ldap.auth import LDAPBackend
|
||||
from authentik.sources.ldap.models import LDAPPropertyMapping, LDAPSource
|
||||
from authentik.sources.ldap.sync.users import UserLDAPSynchronizer
|
||||
from authentik.sources.ldap.tests.mock_ad import mock_ad_connection
|
||||
from authentik.sources.ldap.tests.mock_slapd import mock_slapd_connection
|
||||
|
||||
LDAP_PASSWORD = generate_client_secret()
|
||||
LDAP_PASSWORD = generate_key()
|
||||
|
||||
|
||||
class LDAPSyncTests(TestCase):
|
||||
|
@ -4,12 +4,12 @@ from unittest.mock import PropertyMock, patch
|
||||
from django.test import TestCase
|
||||
|
||||
from authentik.core.models import User
|
||||
from authentik.providers.oauth2.generators import generate_client_secret
|
||||
from authentik.lib.generators import generate_key
|
||||
from authentik.sources.ldap.models import LDAPPropertyMapping, LDAPSource
|
||||
from authentik.sources.ldap.password import LDAPPasswordChanger
|
||||
from authentik.sources.ldap.tests.mock_ad import mock_ad_connection
|
||||
|
||||
LDAP_PASSWORD = generate_client_secret()
|
||||
LDAP_PASSWORD = generate_key()
|
||||
LDAP_CONNECTION_PATCH = PropertyMock(return_value=mock_ad_connection(LDAP_PASSWORD))
|
||||
|
||||
|
||||
|
@ -6,8 +6,8 @@ from django.test import TestCase
|
||||
|
||||
from authentik.core.models import Group, User
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.lib.generators import generate_key
|
||||
from authentik.managed.manager import ObjectManager
|
||||
from authentik.providers.oauth2.generators import generate_client_secret
|
||||
from authentik.sources.ldap.models import LDAPPropertyMapping, LDAPSource
|
||||
from authentik.sources.ldap.sync.groups import GroupLDAPSynchronizer
|
||||
from authentik.sources.ldap.sync.membership import MembershipLDAPSynchronizer
|
||||
@ -16,7 +16,7 @@ from authentik.sources.ldap.tasks import ldap_sync_all
|
||||
from authentik.sources.ldap.tests.mock_ad import mock_ad_connection
|
||||
from authentik.sources.ldap.tests.mock_slapd import mock_slapd_connection
|
||||
|
||||
LDAP_PASSWORD = generate_client_secret()
|
||||
LDAP_PASSWORD = generate_key()
|
||||
|
||||
|
||||
class LDAPSyncTests(TestCase):
|
||||
|
@ -4,7 +4,7 @@ import django.contrib.postgres.fields
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
import authentik.providers.oauth2.generators
|
||||
import authentik.lib.generators
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
@ -33,7 +33,7 @@ class Migration(migrations.Migration):
|
||||
(
|
||||
"client_id",
|
||||
models.TextField(
|
||||
default=authentik.providers.oauth2.generators.generate_client_id,
|
||||
default=authentik.lib.generators.generate_id,
|
||||
help_text="Client identifier used to talk to Plex.",
|
||||
),
|
||||
),
|
||||
|
@ -3,7 +3,7 @@
|
||||
import django.contrib.postgres.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
import authentik.providers.oauth2.generators
|
||||
import authentik.lib.generators
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
@ -11,7 +11,7 @@ from rest_framework.serializers import BaseSerializer
|
||||
from authentik.core.models import Source, UserSourceConnection
|
||||
from authentik.core.types import UILoginButton, UserSettingSerializer
|
||||
from authentik.flows.challenge import Challenge, ChallengeResponse, ChallengeTypes
|
||||
from authentik.providers.oauth2.generators import generate_client_id
|
||||
from authentik.lib.generators import generate_id
|
||||
|
||||
|
||||
class PlexAuthenticationChallenge(Challenge):
|
||||
@ -32,7 +32,7 @@ class PlexSource(Source):
|
||||
"""Authenticate against plex.tv"""
|
||||
|
||||
client_id = models.TextField(
|
||||
default=generate_client_id,
|
||||
default=generate_id,
|
||||
help_text=_("Client identifier used to talk to Plex."),
|
||||
)
|
||||
allowed_servers = ArrayField(
|
||||
|
@ -4,7 +4,7 @@ from requests.exceptions import RequestException
|
||||
from requests_mock import Mocker
|
||||
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.providers.oauth2.generators import generate_client_secret
|
||||
from authentik.lib.generators import generate_key
|
||||
from authentik.sources.plex.models import PlexSource
|
||||
from authentik.sources.plex.plex import PlexAuth
|
||||
from authentik.sources.plex.tasks import check_plex_token_all
|
||||
@ -41,7 +41,7 @@ class TestPlexSource(TestCase):
|
||||
|
||||
def test_get_user_info(self):
|
||||
"""Test get_user_info"""
|
||||
token = generate_client_secret()
|
||||
token = generate_key()
|
||||
api = PlexAuth(self.source, token)
|
||||
with Mocker() as mocker:
|
||||
mocker.get("https://plex.tv/api/v2/user", json=USER_INFO_RESPONSE)
|
||||
@ -55,7 +55,7 @@ class TestPlexSource(TestCase):
|
||||
|
||||
def test_check_server_overlap(self):
|
||||
"""Test check_server_overlap"""
|
||||
token = generate_client_secret()
|
||||
token = generate_key()
|
||||
api = PlexAuth(self.source, token)
|
||||
with Mocker() as mocker:
|
||||
mocker.get("https://plex.tv/api/v2/resources", json=RESOURCES_RESPONSE)
|
||||
|
@ -53,6 +53,11 @@ class SAMLSourceViewSet(UsedByMixin, ModelViewSet):
|
||||
return Response(
|
||||
{
|
||||
"metadata": metadata,
|
||||
"download_url": reverse("authentik_sources_saml:metadata"),
|
||||
"download_url": reverse(
|
||||
"authentik_sources_saml:metadata",
|
||||
kwargs={
|
||||
"source_slug": source.slug,
|
||||
},
|
||||
),
|
||||
}
|
||||
)
|
||||
|
@ -39,7 +39,7 @@ from authentik.sources.saml.processors.constants import (
|
||||
from authentik.sources.saml.processors.request import SESSION_REQUEST_ID
|
||||
from authentik.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND
|
||||
from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
|
||||
from authentik.stages.user_login.stage import BACKEND_DJANGO
|
||||
from authentik.stages.user_login.stage import BACKEND_INBUILT
|
||||
|
||||
LOGGER = get_logger()
|
||||
if TYPE_CHECKING:
|
||||
@ -136,7 +136,7 @@ class ResponseProcessor:
|
||||
self._source.authentication_flow,
|
||||
**{
|
||||
PLAN_CONTEXT_PENDING_USER: user,
|
||||
PLAN_CONTEXT_AUTHENTICATION_BACKEND: BACKEND_DJANGO,
|
||||
PLAN_CONTEXT_AUTHENTICATION_BACKEND: BACKEND_INBUILT,
|
||||
},
|
||||
)
|
||||
|
||||
@ -199,7 +199,7 @@ class ResponseProcessor:
|
||||
self._source.authentication_flow,
|
||||
**{
|
||||
PLAN_CONTEXT_PENDING_USER: matching_users.first(),
|
||||
PLAN_CONTEXT_AUTHENTICATION_BACKEND: BACKEND_DJANGO,
|
||||
PLAN_CONTEXT_AUTHENTICATION_BACKEND: BACKEND_INBUILT,
|
||||
PLAN_CONTEXT_REDIRECT: final_redirect,
|
||||
},
|
||||
)
|
||||
|
@ -1,7 +1,6 @@
|
||||
"""Test validator stage"""
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from django.contrib.sessions.middleware import SessionMiddleware
|
||||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
from django.urls.base import reverse
|
||||
@ -12,8 +11,8 @@ from rest_framework.exceptions import ValidationError
|
||||
from authentik.core.models import User
|
||||
from authentik.flows.challenge import ChallengeTypes
|
||||
from authentik.flows.models import Flow, FlowStageBinding, NotConfiguredAction
|
||||
from authentik.flows.tests.test_planner import dummy_get_response
|
||||
from authentik.providers.oauth2.generators import generate_client_id, generate_client_secret
|
||||
from authentik.lib.generators import generate_id, generate_key
|
||||
from authentik.lib.tests.utils import get_request
|
||||
from authentik.stages.authenticator_duo.models import AuthenticatorDuoStage, DuoDevice
|
||||
from authentik.stages.authenticator_validate.api import AuthenticatorValidateStageSerializer
|
||||
from authentik.stages.authenticator_validate.challenge import (
|
||||
@ -97,11 +96,8 @@ class AuthenticatorValidateStageTests(TestCase):
|
||||
|
||||
def test_device_challenge_webauthn(self):
|
||||
"""Test webauthn"""
|
||||
request = self.request_factory.get("/")
|
||||
request = get_request("/")
|
||||
request.user = self.user
|
||||
middleware = SessionMiddleware(dummy_get_response)
|
||||
middleware.process_request(request)
|
||||
request.session.save()
|
||||
|
||||
webauthn_device = WebAuthnDevice.objects.create(
|
||||
user=self.user,
|
||||
@ -136,8 +132,8 @@ class AuthenticatorValidateStageTests(TestCase):
|
||||
request = self.request_factory.get("/")
|
||||
stage = AuthenticatorDuoStage.objects.create(
|
||||
name="test",
|
||||
client_id=generate_client_id(),
|
||||
client_secret=generate_client_secret(),
|
||||
client_id=generate_id(),
|
||||
client_secret=generate_key(),
|
||||
api_hostname="",
|
||||
)
|
||||
duo_device = DuoDevice.objects.create(
|
||||
|
@ -14,7 +14,7 @@ def inline_static_ascii(path: str) -> str:
|
||||
If no file could be found, original path is returned"""
|
||||
result = Path(finders.find(path))
|
||||
if result:
|
||||
with open(result) as _file:
|
||||
with open(result, encoding="utf8") as _file:
|
||||
return _file.read()
|
||||
return path
|
||||
|
||||
@ -25,7 +25,7 @@ def inline_static_binary(path: str) -> str:
|
||||
path is returned."""
|
||||
result = Path(finders.find(path))
|
||||
if result and result.is_file():
|
||||
with open(result) as _file:
|
||||
with open(result, encoding="utf8") as _file:
|
||||
b64content = b64encode(_file.read().encode())
|
||||
return f"data:image/{result.suffix};base64,{b64content.decode('utf-8')}"
|
||||
return path
|
||||
|
@ -6,10 +6,10 @@ from django.utils.encoding import force_str
|
||||
from authentik.core.models import User
|
||||
from authentik.flows.challenge import ChallengeTypes
|
||||
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding
|
||||
from authentik.providers.oauth2.generators import generate_client_secret
|
||||
from authentik.lib.generators import generate_key
|
||||
from authentik.sources.oauth.models import OAuthSource
|
||||
from authentik.stages.identification.models import IdentificationStage, UserFields
|
||||
from authentik.stages.password import BACKEND_DJANGO
|
||||
from authentik.stages.password import BACKEND_INBUILT
|
||||
from authentik.stages.password.models import PasswordStage
|
||||
|
||||
|
||||
@ -18,7 +18,7 @@ class TestIdentificationStage(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.password = generate_client_secret()
|
||||
self.password = generate_key()
|
||||
self.user = User.objects.create_user(
|
||||
username="unittest", email="test@beryju.org", password=self.password
|
||||
)
|
||||
@ -68,7 +68,7 @@ class TestIdentificationStage(TestCase):
|
||||
|
||||
def test_valid_with_password(self):
|
||||
"""Test with valid email and password in single step"""
|
||||
pw_stage = PasswordStage.objects.create(name="password", backends=[BACKEND_DJANGO])
|
||||
pw_stage = PasswordStage.objects.create(name="password", backends=[BACKEND_INBUILT])
|
||||
self.stage.password_stage = pw_stage
|
||||
self.stage.save()
|
||||
form_data = {"uid_field": self.user.email, "password": self.password}
|
||||
@ -86,7 +86,7 @@ class TestIdentificationStage(TestCase):
|
||||
|
||||
def test_invalid_with_password(self):
|
||||
"""Test with valid email and invalid password in single step"""
|
||||
pw_stage = PasswordStage.objects.create(name="password", backends=[BACKEND_DJANGO])
|
||||
pw_stage = PasswordStage.objects.create(name="password", backends=[BACKEND_INBUILT])
|
||||
self.stage.password_stage = pw_stage
|
||||
self.stage.save()
|
||||
form_data = {
|
||||
|
@ -17,7 +17,7 @@ from authentik.flows.tests.test_views import TO_STAGE_RESPONSE_MOCK
|
||||
from authentik.flows.views import SESSION_KEY_PLAN
|
||||
from authentik.stages.invitation.models import Invitation, InvitationStage
|
||||
from authentik.stages.invitation.stage import INVITATION_TOKEN_KEY, PLAN_CONTEXT_PROMPT
|
||||
from authentik.stages.password import BACKEND_DJANGO
|
||||
from authentik.stages.password import BACKEND_INBUILT
|
||||
from authentik.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND
|
||||
|
||||
|
||||
@ -45,7 +45,7 @@ class TestUserLoginStage(TestCase):
|
||||
"""Test without any invitation, continue_flow_without_invitation not set."""
|
||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
||||
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
|
||||
plan.context[PLAN_CONTEXT_AUTHENTICATION_BACKEND] = BACKEND_DJANGO
|
||||
plan.context[PLAN_CONTEXT_AUTHENTICATION_BACKEND] = BACKEND_INBUILT
|
||||
session = self.client.session
|
||||
session[SESSION_KEY_PLAN] = plan
|
||||
session.save()
|
||||
@ -74,7 +74,7 @@ class TestUserLoginStage(TestCase):
|
||||
self.stage.save()
|
||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
||||
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
|
||||
plan.context[PLAN_CONTEXT_AUTHENTICATION_BACKEND] = BACKEND_DJANGO
|
||||
plan.context[PLAN_CONTEXT_AUTHENTICATION_BACKEND] = BACKEND_INBUILT
|
||||
session = self.client.session
|
||||
session[SESSION_KEY_PLAN] = plan
|
||||
session.save()
|
||||
|
@ -1,3 +1,4 @@
|
||||
"""Backend paths"""
|
||||
BACKEND_DJANGO = "django.contrib.auth.backends.ModelBackend"
|
||||
BACKEND_INBUILT = "authentik.core.auth.InbuiltBackend"
|
||||
BACKEND_LDAP = "authentik.sources.ldap.auth.LDAPBackend"
|
||||
BACKEND_APP_PASSWORD = "authentik.core.auth.TokenBackend" # nosec
|
||||
|
@ -12,6 +12,8 @@ def rename_default_prompt_stage(apps: Apps, schema_editor: BaseDatabaseSchemaEdi
|
||||
if not stages.exists():
|
||||
return
|
||||
stage = stages.first()
|
||||
if PromptStage.objects.using(db_alias).filter(name="default-password-change-prompt").exists():
|
||||
return
|
||||
stage.name = "default-password-change-prompt"
|
||||
stage.save()
|
||||
|
||||
|
48
authentik/stages/password/migrations/0007_app_password.py
Normal file
48
authentik/stages/password/migrations/0007_app_password.py
Normal file
@ -0,0 +1,48 @@
|
||||
# Generated by Django 3.2.6 on 2021-08-23 14:34
|
||||
import django.contrib.postgres.fields
|
||||
from django.apps.registry import Apps
|
||||
from django.db import migrations, models
|
||||
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||
|
||||
from authentik.stages.password import BACKEND_APP_PASSWORD, BACKEND_INBUILT
|
||||
|
||||
|
||||
def update_default_backends(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||
PasswordStage = apps.get_model("authentik_stages_password", "passwordstage")
|
||||
db_alias = schema_editor.connection.alias
|
||||
|
||||
stages = PasswordStage.objects.using(db_alias).filter(name="default-authentication-password")
|
||||
if not stages.exists():
|
||||
return
|
||||
stage = stages.first()
|
||||
stage.backends.append(BACKEND_APP_PASSWORD)
|
||||
stage.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_stages_password", "0006_passwordchange_rename"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="passwordstage",
|
||||
name="backends",
|
||||
field=django.contrib.postgres.fields.ArrayField(
|
||||
base_field=models.TextField(
|
||||
choices=[
|
||||
("authentik.core.auth.InbuiltBackend", "User database + standard password"),
|
||||
("authentik.core.auth.TokenBackend", "User database + app passwords"),
|
||||
(
|
||||
"authentik.sources.ldap.auth.LDAPBackend",
|
||||
"User database + LDAP password",
|
||||
),
|
||||
]
|
||||
),
|
||||
help_text="Selection of backends to test the password against.",
|
||||
size=None,
|
||||
),
|
||||
),
|
||||
migrations.RunPython(update_default_backends),
|
||||
]
|
31
authentik/stages/password/migrations/0008_replace_inbuilt.py
Normal file
31
authentik/stages/password/migrations/0008_replace_inbuilt.py
Normal file
@ -0,0 +1,31 @@
|
||||
# Generated by Django 3.2.6 on 2021-08-23 14:34
|
||||
import django.contrib.postgres.fields
|
||||
from django.apps.registry import Apps
|
||||
from django.db import migrations, models
|
||||
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||
|
||||
from authentik.stages.password import BACKEND_INBUILT
|
||||
|
||||
|
||||
def replace_inbuilt(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||
PasswordStage = apps.get_model("authentik_stages_password", "passwordstage")
|
||||
db_alias = schema_editor.connection.alias
|
||||
|
||||
for stage in PasswordStage.objects.using(db_alias).all():
|
||||
if "django.contrib.auth.backends.ModelBackend" not in stage.backends:
|
||||
continue
|
||||
stage.backends.remove("django.contrib.auth.backends.ModelBackend")
|
||||
stage.backends.append(BACKEND_INBUILT)
|
||||
stage.backends.sort()
|
||||
stage.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_stages_password", "0007_app_password"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(replace_inbuilt),
|
||||
]
|
@ -9,19 +9,23 @@ from rest_framework.serializers import BaseSerializer
|
||||
|
||||
from authentik.core.types import UserSettingSerializer
|
||||
from authentik.flows.models import ConfigurableStage, Stage
|
||||
from authentik.stages.password import BACKEND_DJANGO, BACKEND_LDAP
|
||||
from authentik.stages.password import BACKEND_APP_PASSWORD, BACKEND_INBUILT, BACKEND_LDAP
|
||||
|
||||
|
||||
def get_authentication_backends():
|
||||
"""Return all available authentication backends as tuple set"""
|
||||
return [
|
||||
(
|
||||
BACKEND_DJANGO,
|
||||
_("authentik-internal Userdatabase"),
|
||||
BACKEND_INBUILT,
|
||||
_("User database + standard password"),
|
||||
),
|
||||
(
|
||||
BACKEND_APP_PASSWORD,
|
||||
_("User database + app passwords"),
|
||||
),
|
||||
(
|
||||
BACKEND_LDAP,
|
||||
_("authentik LDAP"),
|
||||
_("User database + LDAP password"),
|
||||
),
|
||||
]
|
||||
|
||||
|
@ -27,6 +27,8 @@ from authentik.stages.password.models import PasswordStage
|
||||
|
||||
LOGGER = get_logger()
|
||||
PLAN_CONTEXT_AUTHENTICATION_BACKEND = "user_backend"
|
||||
PLAN_CONTEXT_METHOD = "auth_method"
|
||||
PLAN_CONTEXT_METHOD_ARGS = "auth_method_args"
|
||||
SESSION_INVALID_TRIES = "user_invalid_tries"
|
||||
|
||||
|
||||
|
@ -13,8 +13,8 @@ from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding
|
||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
|
||||
from authentik.flows.tests.test_views import TO_STAGE_RESPONSE_MOCK
|
||||
from authentik.flows.views import SESSION_KEY_PLAN
|
||||
from authentik.providers.oauth2.generators import generate_client_secret
|
||||
from authentik.stages.password import BACKEND_DJANGO
|
||||
from authentik.lib.generators import generate_key
|
||||
from authentik.stages.password import BACKEND_INBUILT
|
||||
from authentik.stages.password.models import PasswordStage
|
||||
|
||||
MOCK_BACKEND_AUTHENTICATE = MagicMock(side_effect=PermissionDenied("test"))
|
||||
@ -25,7 +25,7 @@ class TestPasswordStage(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.password = generate_client_secret()
|
||||
self.password = generate_key()
|
||||
self.user = User.objects.create_user(
|
||||
username="unittest", email="test@beryju.org", password=self.password
|
||||
)
|
||||
@ -36,7 +36,7 @@ class TestPasswordStage(TestCase):
|
||||
slug="test-password",
|
||||
designation=FlowDesignation.AUTHENTICATION,
|
||||
)
|
||||
self.stage = PasswordStage.objects.create(name="password", backends=[BACKEND_DJANGO])
|
||||
self.stage = PasswordStage.objects.create(name="password", backends=[BACKEND_INBUILT])
|
||||
self.binding = FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2)
|
||||
|
||||
@patch(
|
||||
@ -158,7 +158,7 @@ class TestPasswordStage(TestCase):
|
||||
TO_STAGE_RESPONSE_MOCK,
|
||||
)
|
||||
@patch(
|
||||
"django.contrib.auth.backends.ModelBackend.authenticate",
|
||||
"authentik.core.auth.InbuiltBackend.authenticate",
|
||||
MOCK_BACKEND_AUTHENTICATE,
|
||||
)
|
||||
def test_permission_denied(self):
|
||||
|
@ -8,7 +8,7 @@ from structlog.stdlib import get_logger
|
||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
|
||||
from authentik.flows.stage import StageView
|
||||
from authentik.lib.utils.time import timedelta_from_string
|
||||
from authentik.stages.password import BACKEND_DJANGO
|
||||
from authentik.stages.password import BACKEND_INBUILT
|
||||
from authentik.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND
|
||||
|
||||
LOGGER = get_logger()
|
||||
@ -26,7 +26,7 @@ class UserLoginStageView(StageView):
|
||||
LOGGER.debug(message)
|
||||
return self.executor.stage_invalid()
|
||||
backend = self.executor.plan.context.get(
|
||||
PLAN_CONTEXT_AUTHENTICATION_BACKEND, BACKEND_DJANGO
|
||||
PLAN_CONTEXT_AUTHENTICATION_BACKEND, BACKEND_INBUILT
|
||||
)
|
||||
login(
|
||||
self.request,
|
||||
@ -40,6 +40,7 @@ class UserLoginStageView(StageView):
|
||||
self.request.session.set_expiry(delta)
|
||||
LOGGER.debug(
|
||||
"Logged in",
|
||||
backend=backend,
|
||||
user=self.executor.plan.context[PLAN_CONTEXT_PENDING_USER],
|
||||
flow_slug=self.executor.flow.slug,
|
||||
session_duration=self.executor.current_stage.session_duration,
|
||||
|
@ -9,7 +9,7 @@ from authentik.flows.markers import StageMarker
|
||||
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding
|
||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
|
||||
from authentik.flows.views import SESSION_KEY_PLAN
|
||||
from authentik.stages.password import BACKEND_DJANGO
|
||||
from authentik.stages.password import BACKEND_INBUILT
|
||||
from authentik.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND
|
||||
from authentik.stages.user_logout.models import UserLogoutStage
|
||||
|
||||
@ -34,7 +34,7 @@ class TestUserLogoutStage(TestCase):
|
||||
"""Test with a valid pending user and backend"""
|
||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
||||
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
|
||||
plan.context[PLAN_CONTEXT_AUTHENTICATION_BACKEND] = BACKEND_DJANGO
|
||||
plan.context[PLAN_CONTEXT_AUTHENTICATION_BACKEND] = BACKEND_INBUILT
|
||||
session = self.client.session
|
||||
session[SESSION_KEY_PLAN] = plan
|
||||
session.save()
|
||||
|
@ -1,7 +1,6 @@
|
||||
"""Write stage logic"""
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth import update_session_auth_hash
|
||||
from django.contrib.auth.backends import ModelBackend
|
||||
from django.db import transaction
|
||||
from django.db.utils import IntegrityError
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
@ -13,7 +12,7 @@ from authentik.core.models import USER_ATTRIBUTE_SOURCES, User, UserSourceConnec
|
||||
from authentik.core.sources.stage import PLAN_CONTEXT_SOURCES_CONNECTION
|
||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
|
||||
from authentik.flows.stage import StageView
|
||||
from authentik.lib.utils.reflection import class_to_path
|
||||
from authentik.stages.password import BACKEND_INBUILT
|
||||
from authentik.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND
|
||||
from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
|
||||
from authentik.stages.user_write.signals import user_write
|
||||
@ -42,9 +41,7 @@ class UserWriteStageView(StageView):
|
||||
self.executor.plan.context[PLAN_CONTEXT_PENDING_USER] = User(
|
||||
is_active=not self.executor.current_stage.create_users_as_inactive
|
||||
)
|
||||
self.executor.plan.context[PLAN_CONTEXT_AUTHENTICATION_BACKEND] = class_to_path(
|
||||
ModelBackend
|
||||
)
|
||||
self.executor.plan.context[PLAN_CONTEXT_AUTHENTICATION_BACKEND] = BACKEND_INBUILT
|
||||
LOGGER.debug(
|
||||
"Created new user",
|
||||
flow_slug=self.executor.flow.slug,
|
||||
|
@ -309,9 +309,7 @@ stages:
|
||||
displayName: Build static files for e2e
|
||||
inputs:
|
||||
script: |
|
||||
make gen-web
|
||||
cd web
|
||||
cd api && npm i && cd ..
|
||||
npm i
|
||||
npm run build
|
||||
- task: CmdLine@2
|
||||
@ -397,17 +395,6 @@ stages:
|
||||
- task: CmdLine@2
|
||||
inputs:
|
||||
script: bash <(curl -s https://codecov.io/bash)
|
||||
- task: CmdLine@2
|
||||
continueOnError: true
|
||||
inputs:
|
||||
script: |
|
||||
npm install -g @zeus-ci/cli
|
||||
npx zeus job update -b $BUILD_BUILDID -j $BUILD_BUILDNUMBER -r $BUILD_SOURCEVERSION
|
||||
npx zeus upload -b $BUILD_BUILDID -j $BUILD_BUILDNUMBER -t "application/x-cobertura+xml" coverage.xml
|
||||
npx zeus upload -b $BUILD_BUILDID -j $BUILD_BUILDNUMBER -t "application/x-junit+xml" coverage-e2e/unittest.xml
|
||||
npx zeus upload -b $BUILD_BUILDID -j $BUILD_BUILDNUMBER -t "application/x-junit+xml" coverage-integration/unittest.xml
|
||||
npx zeus upload -b $BUILD_BUILDID -j $BUILD_BUILDNUMBER -t "application/x-junit+xml" coverage-unittest/unittest.xml
|
||||
npx zeus job update --status=passed -b $BUILD_BUILDID -j $BUILD_BUILDNUMBER -r $BUILD_SOURCEVERSION
|
||||
- stage: Build
|
||||
jobs:
|
||||
- job: build_server
|
||||
|
@ -10,7 +10,7 @@ services:
|
||||
networks:
|
||||
- internal
|
||||
environment:
|
||||
- POSTGRES_PASSWORD=${PG_PASS:-thisisnotagoodpassword}
|
||||
- POSTGRES_PASSWORD=${PG_PASS:?database password required}
|
||||
- POSTGRES_USER=${PG_USER:-authentik}
|
||||
- POSTGRES_DB=${PG_DB:-authentik}
|
||||
env_file:
|
||||
@ -21,7 +21,7 @@ services:
|
||||
networks:
|
||||
- internal
|
||||
server:
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2021.8.1-rc1}
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2021.8.1}
|
||||
restart: unless-stopped
|
||||
command: server
|
||||
environment:
|
||||
@ -44,7 +44,7 @@ services:
|
||||
- "0.0.0.0:9000:9000"
|
||||
- "0.0.0.0:9443:9443"
|
||||
worker:
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2021.8.1-rc1}
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2021.8.1}
|
||||
restart: unless-stopped
|
||||
command: worker
|
||||
networks:
|
||||
|
2
go.mod
2
go.mod
@ -11,7 +11,7 @@ require (
|
||||
github.com/go-openapi/analysis v0.20.1 // indirect
|
||||
github.com/go-openapi/errors v0.20.0 // indirect
|
||||
github.com/go-openapi/runtime v0.19.30
|
||||
github.com/go-openapi/strfmt v0.20.1
|
||||
github.com/go-openapi/strfmt v0.20.2
|
||||
github.com/go-openapi/swag v0.19.15 // indirect
|
||||
github.com/go-openapi/validate v0.20.2 // indirect
|
||||
github.com/go-redis/redis/v7 v7.4.0 // indirect
|
||||
|
4
go.sum
4
go.sum
@ -227,8 +227,8 @@ github.com/go-openapi/strfmt v0.19.4/go.mod h1:eftuHTlB/dI8Uq8JJOyRlieZf+WkkxUuk
|
||||
github.com/go-openapi/strfmt v0.19.5/go.mod h1:eftuHTlB/dI8Uq8JJOyRlieZf+WkkxUuk0dgdHXr2Qk=
|
||||
github.com/go-openapi/strfmt v0.19.11/go.mod h1:UukAYgTaQfqJuAFlNxxMWNvMYiwiXtLsF2VwmoFtbtc=
|
||||
github.com/go-openapi/strfmt v0.20.0/go.mod h1:UukAYgTaQfqJuAFlNxxMWNvMYiwiXtLsF2VwmoFtbtc=
|
||||
github.com/go-openapi/strfmt v0.20.1 h1:1VgxvehFne1mbChGeCmZ5pc0LxUf6yaACVSIYAR91Xc=
|
||||
github.com/go-openapi/strfmt v0.20.1/go.mod h1:43urheQI9dNtE5lTZQfuFJvjYJKPrxicATpEfZwHUNk=
|
||||
github.com/go-openapi/strfmt v0.20.2 h1:6XZL+fF4VZYFxKQGLAUB358hOrRh/wS51uWEtlONADE=
|
||||
github.com/go-openapi/strfmt v0.20.2/go.mod h1:43urheQI9dNtE5lTZQfuFJvjYJKPrxicATpEfZwHUNk=
|
||||
github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg=
|
||||
github.com/go-openapi/swag v0.18.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg=
|
||||
github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
|
||||
|
@ -17,4 +17,4 @@ func OutpostUserAgent() string {
|
||||
return fmt.Sprintf("authentik-outpost@%s (%s)", VERSION, BUILD())
|
||||
}
|
||||
|
||||
const VERSION = "2021.8.1-rc1"
|
||||
const VERSION = "2021.8.1"
|
||||
|
@ -28,7 +28,7 @@ func NewGoUnicorn() *GoUnicorn {
|
||||
|
||||
func (g *GoUnicorn) initCmd() {
|
||||
command := "gunicorn"
|
||||
args := []string{"-c", "./lifecycle/gunicorn.conf.py", "authentik.root.asgi:application"}
|
||||
args := []string{"-c", "./lifecycle/gunicorn.conf.py", "authentik.root.asgi.app:application"}
|
||||
if config.G.Debug {
|
||||
command = "python"
|
||||
args = []string{"manage.py", "runserver", "localhost:8000"}
|
||||
|
@ -23,11 +23,12 @@ type BindRequest struct {
|
||||
func (ls *LDAPServer) Bind(bindDN string, bindPW string, conn net.Conn) (ldap.LDAPResultCode, error) {
|
||||
span := sentry.StartSpan(context.TODO(), "authentik.providers.ldap.bind",
|
||||
sentry.TransactionName("authentik.providers.ldap.bind"))
|
||||
rid := uuid.New().String()
|
||||
span.SetTag("request_uid", rid)
|
||||
span.SetTag("user.username", bindDN)
|
||||
defer span.Finish()
|
||||
|
||||
bindDN = strings.ToLower(bindDN)
|
||||
rid := uuid.New().String()
|
||||
req := BindRequest{
|
||||
BindDN: bindDN,
|
||||
BindPW: bindPW,
|
||||
|
@ -24,12 +24,13 @@ type SearchRequest struct {
|
||||
|
||||
func (ls *LDAPServer) Search(bindDN string, searchReq ldap.SearchRequest, conn net.Conn) (ldap.ServerSearchResult, error) {
|
||||
span := sentry.StartSpan(context.TODO(), "authentik.providers.ldap.search", sentry.TransactionName("authentik.providers.ldap.search"))
|
||||
rid := uuid.New().String()
|
||||
span.SetTag("request_uid", rid)
|
||||
span.SetTag("user.username", bindDN)
|
||||
span.SetTag("ak_filter", searchReq.Filter)
|
||||
span.SetTag("ak_base_dn", searchReq.BaseDN)
|
||||
|
||||
bindDN = strings.ToLower(bindDN)
|
||||
rid := uuid.New().String()
|
||||
req := SearchRequest{
|
||||
SearchRequest: searchReq,
|
||||
BindDN: bindDN,
|
||||
|
123
schema.yml
123
schema.yml
@ -1,7 +1,7 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: authentik
|
||||
version: 2021.8.1-rc1
|
||||
version: 2021.8.1
|
||||
description: Making authentication simple.
|
||||
contact:
|
||||
email: hello@beryju.org
|
||||
@ -1710,7 +1710,7 @@ paths:
|
||||
content:
|
||||
multipart/form-data:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SetIconRequest'
|
||||
$ref: '#/components/schemas/FileUploadRequest'
|
||||
security:
|
||||
- authentik: []
|
||||
- cookieAuth: []
|
||||
@ -1738,13 +1738,13 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SetIconURLRequest'
|
||||
$ref: '#/components/schemas/FilePathRequest'
|
||||
application/x-www-form-urlencoded:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SetIconURLRequest'
|
||||
$ref: '#/components/schemas/FilePathRequest'
|
||||
multipart/form-data:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SetIconURLRequest'
|
||||
$ref: '#/components/schemas/FilePathRequest'
|
||||
required: true
|
||||
security:
|
||||
- authentik: []
|
||||
@ -2515,6 +2515,7 @@ paths:
|
||||
type: string
|
||||
enum:
|
||||
- api
|
||||
- app_password
|
||||
- recovery
|
||||
- verification
|
||||
- name: ordering
|
||||
@ -3281,6 +3282,38 @@ paths:
|
||||
$ref: '#/components/schemas/ValidationError'
|
||||
'403':
|
||||
$ref: '#/components/schemas/GenericError'
|
||||
/api/v2beta/core/users/service_account/:
|
||||
post:
|
||||
operationId: core_users_service_account_create
|
||||
description: Create a new user account that is marked as a service account
|
||||
tags:
|
||||
- core
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/UserServiceAccountRequest'
|
||||
application/x-www-form-urlencoded:
|
||||
schema:
|
||||
$ref: '#/components/schemas/UserServiceAccountRequest'
|
||||
multipart/form-data:
|
||||
schema:
|
||||
$ref: '#/components/schemas/UserServiceAccountRequest'
|
||||
required: true
|
||||
security:
|
||||
- authentik: []
|
||||
- cookieAuth: []
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/UserServiceAccountResponse'
|
||||
description: ''
|
||||
'400':
|
||||
$ref: '#/components/schemas/ValidationError'
|
||||
'403':
|
||||
$ref: '#/components/schemas/GenericError'
|
||||
/api/v2beta/core/users/update_self/:
|
||||
put:
|
||||
operationId: core_users_update_self_update
|
||||
@ -5459,7 +5492,7 @@ paths:
|
||||
content:
|
||||
multipart/form-data:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SetIconRequest'
|
||||
$ref: '#/components/schemas/FileUploadRequest'
|
||||
security:
|
||||
- authentik: []
|
||||
- cookieAuth: []
|
||||
@ -5487,13 +5520,13 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SetIconURLRequest'
|
||||
$ref: '#/components/schemas/FilePathRequest'
|
||||
application/x-www-form-urlencoded:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SetIconURLRequest'
|
||||
$ref: '#/components/schemas/FilePathRequest'
|
||||
multipart/form-data:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SetIconURLRequest'
|
||||
$ref: '#/components/schemas/FilePathRequest'
|
||||
required: true
|
||||
security:
|
||||
- authentik: []
|
||||
@ -5580,7 +5613,7 @@ paths:
|
||||
content:
|
||||
multipart/form-data:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SetIconRequest'
|
||||
$ref: '#/components/schemas/FileUploadRequest'
|
||||
security:
|
||||
- authentik: []
|
||||
- cookieAuth: []
|
||||
@ -20381,7 +20414,8 @@ components:
|
||||
- url
|
||||
BackendsEnum:
|
||||
enum:
|
||||
- django.contrib.auth.backends.ModelBackend
|
||||
- authentik.core.auth.InbuiltBackend
|
||||
- authentik.core.auth.TokenBackend
|
||||
- authentik.sources.ldap.auth.LDAPBackend
|
||||
type: string
|
||||
BindingTypeEnum:
|
||||
@ -21549,6 +21583,24 @@ components:
|
||||
type: string
|
||||
required:
|
||||
- expression
|
||||
FilePathRequest:
|
||||
type: object
|
||||
description: Serializer to upload file
|
||||
properties:
|
||||
url:
|
||||
type: string
|
||||
required:
|
||||
- url
|
||||
FileUploadRequest:
|
||||
type: object
|
||||
description: Serializer to upload file
|
||||
properties:
|
||||
file:
|
||||
type: string
|
||||
format: binary
|
||||
clear:
|
||||
type: boolean
|
||||
default: false
|
||||
Flow:
|
||||
type: object
|
||||
description: Flow Serializer
|
||||
@ -22211,6 +22263,7 @@ components:
|
||||
- verification
|
||||
- api
|
||||
- recovery
|
||||
- app_password
|
||||
type: string
|
||||
InvalidResponseActionEnum:
|
||||
enum:
|
||||
@ -27841,6 +27894,8 @@ components:
|
||||
intent:
|
||||
$ref: '#/components/schemas/IntentEnum'
|
||||
user:
|
||||
type: integer
|
||||
user_obj:
|
||||
$ref: '#/components/schemas/UserRequest'
|
||||
description:
|
||||
type: string
|
||||
@ -29545,22 +29600,6 @@ components:
|
||||
$ref: '#/components/schemas/UserSelf'
|
||||
required:
|
||||
- user
|
||||
SetIconRequest:
|
||||
type: object
|
||||
properties:
|
||||
file:
|
||||
type: string
|
||||
format: binary
|
||||
clear:
|
||||
type: boolean
|
||||
default: false
|
||||
SetIconURLRequest:
|
||||
type: object
|
||||
properties:
|
||||
url:
|
||||
type: string
|
||||
required:
|
||||
- url
|
||||
SeverityEnum:
|
||||
enum:
|
||||
- notice
|
||||
@ -29819,6 +29858,11 @@ components:
|
||||
type: object
|
||||
description: Get system information.
|
||||
properties:
|
||||
env:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: string
|
||||
readOnly: true
|
||||
http_headers:
|
||||
type: object
|
||||
additionalProperties:
|
||||
@ -29858,6 +29902,7 @@ components:
|
||||
readOnly: true
|
||||
required:
|
||||
- embedded_outpost_host
|
||||
- env
|
||||
- http_headers
|
||||
- http_host
|
||||
- http_is_secure
|
||||
@ -30014,6 +30059,8 @@ components:
|
||||
intent:
|
||||
$ref: '#/components/schemas/IntentEnum'
|
||||
user:
|
||||
type: integer
|
||||
user_obj:
|
||||
$ref: '#/components/schemas/User'
|
||||
description:
|
||||
type: string
|
||||
@ -30044,6 +30091,8 @@ components:
|
||||
intent:
|
||||
$ref: '#/components/schemas/IntentEnum'
|
||||
user:
|
||||
type: integer
|
||||
user_obj:
|
||||
$ref: '#/components/schemas/UserRequest'
|
||||
description:
|
||||
type: string
|
||||
@ -30533,6 +30582,26 @@ components:
|
||||
required:
|
||||
- name
|
||||
- username
|
||||
UserServiceAccountRequest:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
create_group:
|
||||
type: boolean
|
||||
default: false
|
||||
required:
|
||||
- name
|
||||
UserServiceAccountResponse:
|
||||
type: object
|
||||
properties:
|
||||
username:
|
||||
type: string
|
||||
token:
|
||||
type: string
|
||||
required:
|
||||
- token
|
||||
- username
|
||||
UserSetting:
|
||||
type: object
|
||||
description: Serializer for User settings for stages and sources
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user