Compare commits
1 Commits
version/20
...
root/confi
Author | SHA1 | Date | |
---|---|---|---|
3fa987f443 |
@ -1,5 +1,5 @@
|
|||||||
[bumpversion]
|
[bumpversion]
|
||||||
current_version = 2023.3.0
|
current_version = 2023.1.2
|
||||||
tag = True
|
tag = True
|
||||||
commit = True
|
commit = True
|
||||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)
|
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)
|
||||||
|
2
.github/actions/setup/action.yml
vendored
2
.github/actions/setup/action.yml
vendored
@ -18,7 +18,7 @@ runs:
|
|||||||
- name: Setup node
|
- name: Setup node
|
||||||
uses: actions/setup-node@v3.1.0
|
uses: actions/setup-node@v3.1.0
|
||||||
with:
|
with:
|
||||||
node-version: '18'
|
node-version: '16'
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
cache-dependency-path: web/package-lock.json
|
cache-dependency-path: web/package-lock.json
|
||||||
- name: Setup dependencies
|
- name: Setup dependencies
|
||||||
|
7
.github/workflows/ci-main.yml
vendored
7
.github/workflows/ci-main.yml
vendored
@ -80,7 +80,6 @@ jobs:
|
|||||||
run: poetry run python -m lifecycle.migrate
|
run: poetry run python -m lifecycle.migrate
|
||||||
test-unittest:
|
test-unittest:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 30
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- name: Setup authentik env
|
- name: Setup authentik env
|
||||||
@ -95,7 +94,6 @@ jobs:
|
|||||||
flags: unit
|
flags: unit
|
||||||
test-integration:
|
test-integration:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 30
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- name: Setup authentik env
|
- name: Setup authentik env
|
||||||
@ -113,7 +111,6 @@ jobs:
|
|||||||
test-e2e:
|
test-e2e:
|
||||||
name: test-e2e (${{ matrix.job.name }})
|
name: test-e2e (${{ matrix.job.name }})
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 30
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@ -191,7 +188,7 @@ jobs:
|
|||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: Build Docker Image
|
- name: Build Docker Image
|
||||||
uses: docker/build-push-action@v4
|
uses: docker/build-push-action@v3
|
||||||
with:
|
with:
|
||||||
secrets: |
|
secrets: |
|
||||||
GEOIPUPDATE_ACCOUNT_ID=${{ secrets.GEOIPUPDATE_ACCOUNT_ID }}
|
GEOIPUPDATE_ACCOUNT_ID=${{ secrets.GEOIPUPDATE_ACCOUNT_ID }}
|
||||||
@ -232,7 +229,7 @@ jobs:
|
|||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: Build Docker Image
|
- name: Build Docker Image
|
||||||
uses: docker/build-push-action@v4
|
uses: docker/build-push-action@v3
|
||||||
with:
|
with:
|
||||||
secrets: |
|
secrets: |
|
||||||
GEOIPUPDATE_ACCOUNT_ID=${{ secrets.GEOIPUPDATE_ACCOUNT_ID }}
|
GEOIPUPDATE_ACCOUNT_ID=${{ secrets.GEOIPUPDATE_ACCOUNT_ID }}
|
||||||
|
9
.github/workflows/ci-outpost.yml
vendored
9
.github/workflows/ci-outpost.yml
vendored
@ -49,7 +49,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- run: echo mark
|
- run: echo mark
|
||||||
build-container:
|
build:
|
||||||
timeout-minutes: 120
|
timeout-minutes: 120
|
||||||
needs:
|
needs:
|
||||||
- ci-outpost-mark
|
- ci-outpost-mark
|
||||||
@ -83,7 +83,7 @@ jobs:
|
|||||||
- name: Generate API
|
- name: Generate API
|
||||||
run: make gen-client-go
|
run: make gen-client-go
|
||||||
- name: Build Docker Image
|
- name: Build Docker Image
|
||||||
uses: docker/build-push-action@v4
|
uses: docker/build-push-action@v3
|
||||||
with:
|
with:
|
||||||
push: ${{ steps.ev.outputs.shouldBuild == 'true' }}
|
push: ${{ steps.ev.outputs.shouldBuild == 'true' }}
|
||||||
tags: |
|
tags: |
|
||||||
@ -94,8 +94,7 @@ jobs:
|
|||||||
GIT_BUILD_HASH=${{ steps.ev.outputs.sha }}
|
GIT_BUILD_HASH=${{ steps.ev.outputs.sha }}
|
||||||
VERSION_FAMILY=${{ steps.ev.outputs.versionFamily }}
|
VERSION_FAMILY=${{ steps.ev.outputs.versionFamily }}
|
||||||
platforms: ${{ matrix.arch }}
|
platforms: ${{ matrix.arch }}
|
||||||
context: .
|
build-outpost-binary:
|
||||||
build-binary:
|
|
||||||
timeout-minutes: 120
|
timeout-minutes: 120
|
||||||
needs:
|
needs:
|
||||||
- ci-outpost-mark
|
- ci-outpost-mark
|
||||||
@ -115,7 +114,7 @@ jobs:
|
|||||||
go-version: "^1.17"
|
go-version: "^1.17"
|
||||||
- uses: actions/setup-node@v3.6.0
|
- uses: actions/setup-node@v3.6.0
|
||||||
with:
|
with:
|
||||||
node-version: '18'
|
node-version: '16'
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
cache-dependency-path: web/package-lock.json
|
cache-dependency-path: web/package-lock.json
|
||||||
- name: Generate API
|
- name: Generate API
|
||||||
|
10
.github/workflows/ci-web.yml
vendored
10
.github/workflows/ci-web.yml
vendored
@ -17,7 +17,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-node@v3.6.0
|
- uses: actions/setup-node@v3.6.0
|
||||||
with:
|
with:
|
||||||
node-version: '18'
|
node-version: '16'
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
cache-dependency-path: web/package-lock.json
|
cache-dependency-path: web/package-lock.json
|
||||||
- working-directory: web/
|
- working-directory: web/
|
||||||
@ -33,7 +33,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-node@v3.6.0
|
- uses: actions/setup-node@v3.6.0
|
||||||
with:
|
with:
|
||||||
node-version: '18'
|
node-version: '16'
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
cache-dependency-path: web/package-lock.json
|
cache-dependency-path: web/package-lock.json
|
||||||
- working-directory: web/
|
- working-directory: web/
|
||||||
@ -49,7 +49,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-node@v3.6.0
|
- uses: actions/setup-node@v3.6.0
|
||||||
with:
|
with:
|
||||||
node-version: '18'
|
node-version: '16'
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
cache-dependency-path: web/package-lock.json
|
cache-dependency-path: web/package-lock.json
|
||||||
- working-directory: web/
|
- working-directory: web/
|
||||||
@ -65,7 +65,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-node@v3.6.0
|
- uses: actions/setup-node@v3.6.0
|
||||||
with:
|
with:
|
||||||
node-version: '18'
|
node-version: '16'
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
cache-dependency-path: web/package-lock.json
|
cache-dependency-path: web/package-lock.json
|
||||||
- working-directory: web/
|
- working-directory: web/
|
||||||
@ -97,7 +97,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-node@v3.6.0
|
- uses: actions/setup-node@v3.6.0
|
||||||
with:
|
with:
|
||||||
node-version: '18'
|
node-version: '16'
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
cache-dependency-path: web/package-lock.json
|
cache-dependency-path: web/package-lock.json
|
||||||
- working-directory: web/
|
- working-directory: web/
|
||||||
|
17
.github/workflows/ci-website.yml
vendored
17
.github/workflows/ci-website.yml
vendored
@ -17,7 +17,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-node@v3.6.0
|
- uses: actions/setup-node@v3.6.0
|
||||||
with:
|
with:
|
||||||
node-version: '18'
|
node-version: '16'
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
cache-dependency-path: website/package-lock.json
|
cache-dependency-path: website/package-lock.json
|
||||||
- working-directory: website/
|
- working-directory: website/
|
||||||
@ -25,24 +25,9 @@ jobs:
|
|||||||
- name: prettier
|
- name: prettier
|
||||||
working-directory: website/
|
working-directory: website/
|
||||||
run: npm run prettier-check
|
run: npm run prettier-check
|
||||||
test:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- uses: actions/setup-node@v3.6.0
|
|
||||||
with:
|
|
||||||
node-version: '18'
|
|
||||||
cache: 'npm'
|
|
||||||
cache-dependency-path: website/package-lock.json
|
|
||||||
- working-directory: website/
|
|
||||||
run: npm ci
|
|
||||||
- name: test
|
|
||||||
working-directory: website/
|
|
||||||
run: npm test
|
|
||||||
ci-website-mark:
|
ci-website-mark:
|
||||||
needs:
|
needs:
|
||||||
- lint-prettier
|
- lint-prettier
|
||||||
- test
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- run: echo mark
|
- run: echo mark
|
||||||
|
2
.github/workflows/ghcr-retention.yml
vendored
2
.github/workflows/ghcr-retention.yml
vendored
@ -11,7 +11,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Delete 'dev' containers older than a week
|
- name: Delete 'dev' containers older than a week
|
||||||
uses: snok/container-retention-policy@v2
|
uses: snok/container-retention-policy@v1
|
||||||
with:
|
with:
|
||||||
image-names: dev-server,dev-ldap,dev-proxy
|
image-names: dev-server,dev-ldap,dev-proxy
|
||||||
cut-off: One week ago UTC
|
cut-off: One week ago UTC
|
||||||
|
6
.github/workflows/release-publish.yml
vendored
6
.github/workflows/release-publish.yml
vendored
@ -28,7 +28,7 @@ jobs:
|
|||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: Build Docker Image
|
- name: Build Docker Image
|
||||||
uses: docker/build-push-action@v4
|
uses: docker/build-push-action@v3
|
||||||
with:
|
with:
|
||||||
push: ${{ github.event_name == 'release' }}
|
push: ${{ github.event_name == 'release' }}
|
||||||
secrets: |
|
secrets: |
|
||||||
@ -76,7 +76,7 @@ jobs:
|
|||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: Build Docker Image
|
- name: Build Docker Image
|
||||||
uses: docker/build-push-action@v4
|
uses: docker/build-push-action@v3
|
||||||
with:
|
with:
|
||||||
push: ${{ github.event_name == 'release' }}
|
push: ${{ github.event_name == 'release' }}
|
||||||
tags: |
|
tags: |
|
||||||
@ -108,7 +108,7 @@ jobs:
|
|||||||
go-version: "^1.17"
|
go-version: "^1.17"
|
||||||
- uses: actions/setup-node@v3.6.0
|
- uses: actions/setup-node@v3.6.0
|
||||||
with:
|
with:
|
||||||
node-version: '18'
|
node-version: '16'
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
cache-dependency-path: web/package-lock.json
|
cache-dependency-path: web/package-lock.json
|
||||||
- name: Build web
|
- name: Build web
|
||||||
|
2
.github/workflows/web-api-publish.yml
vendored
2
.github/workflows/web-api-publish.yml
vendored
@ -14,7 +14,7 @@ jobs:
|
|||||||
token: ${{ secrets.BOT_GITHUB_TOKEN }}
|
token: ${{ secrets.BOT_GITHUB_TOKEN }}
|
||||||
- uses: actions/setup-node@v3.6.0
|
- uses: actions/setup-node@v3.6.0
|
||||||
with:
|
with:
|
||||||
node-version: '18'
|
node-version: '16'
|
||||||
registry-url: 'https://registry.npmjs.org'
|
registry-url: 'https://registry.npmjs.org'
|
||||||
- name: Generate API Client
|
- name: Generate API Client
|
||||||
run: make gen-client-ts
|
run: make gen-client-ts
|
||||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -200,6 +200,3 @@ media/
|
|||||||
.idea/
|
.idea/
|
||||||
/gen-*/
|
/gen-*/
|
||||||
data/
|
data/
|
||||||
|
|
||||||
# Local Netlify folder
|
|
||||||
.netlify
|
|
||||||
|
20
.vscode/extensions.json
vendored
20
.vscode/extensions.json
vendored
@ -1,20 +0,0 @@
|
|||||||
{
|
|
||||||
"recommendations": [
|
|
||||||
"EditorConfig.EditorConfig",
|
|
||||||
"bashmish.es6-string-css",
|
|
||||||
"bpruitt-goddard.mermaid-markdown-syntax-highlighting",
|
|
||||||
"dbaeumer.vscode-eslint",
|
|
||||||
"esbenp.prettier-vscode",
|
|
||||||
"golang.go",
|
|
||||||
"Gruntfuggly.todo-tree",
|
|
||||||
"mechatroner.rainbow-csv",
|
|
||||||
"ms-python.black-formatter",
|
|
||||||
"ms-python.isort",
|
|
||||||
"ms-python.pylint",
|
|
||||||
"ms-python.python",
|
|
||||||
"ms-python.vscode-pylance",
|
|
||||||
"redhat.vscode-yaml",
|
|
||||||
"Tobermory.es6-string-html",
|
|
||||||
"unifiedjs.vscode-mdx"
|
|
||||||
]
|
|
||||||
}
|
|
6
.vscode/settings.json
vendored
6
.vscode/settings.json
vendored
@ -16,8 +16,7 @@
|
|||||||
"passwordless",
|
"passwordless",
|
||||||
"kubernetes",
|
"kubernetes",
|
||||||
"sso",
|
"sso",
|
||||||
"slo",
|
"slo"
|
||||||
"scim",
|
|
||||||
],
|
],
|
||||||
"python.linting.pylintEnabled": true,
|
"python.linting.pylintEnabled": true,
|
||||||
"todo-tree.tree.showCountsInTree": true,
|
"todo-tree.tree.showCountsInTree": true,
|
||||||
@ -47,6 +46,5 @@
|
|||||||
"url": "https://github.com/goauthentik/authentik/issues/<num>",
|
"url": "https://github.com/goauthentik/authentik/issues/<num>",
|
||||||
"ignoreCase": false
|
"ignoreCase": false
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
"go.testFlags": ["-count=1"]
|
|
||||||
}
|
}
|
||||||
|
@ -154,19 +154,12 @@ While the prerequisites above must be satisfied prior to having your pull reques
|
|||||||
|
|
||||||
## Styleguides
|
## Styleguides
|
||||||
|
|
||||||
### PR naming
|
|
||||||
|
|
||||||
- Use the format of `<package>: <verb> <description>`
|
|
||||||
- See [here](#authentik-packages) for `package`
|
|
||||||
- Example: `providers/saml2: fix parsing of requests`
|
|
||||||
|
|
||||||
### Git Commit Messages
|
### Git Commit Messages
|
||||||
|
|
||||||
- Use the format of `<package>: <verb> <description>`
|
- Use the format of `<package>: <verb> <description>`
|
||||||
- See [here](#authentik-packages) for `package`
|
- See [here](#authentik-packages) for `package`
|
||||||
- Example: `providers/saml2: fix parsing of requests`
|
- Example: `providers/saml2: fix parsing of requests`
|
||||||
- Reference issues and pull requests liberally after the first line
|
- Reference issues and pull requests liberally after the first line
|
||||||
- Naming of commits within a PR does not need to adhere to the guidelines as we squash merge PRs
|
|
||||||
|
|
||||||
### Python Styleguide
|
### Python Styleguide
|
||||||
|
|
||||||
|
@ -20,7 +20,7 @@ WORKDIR /work/web
|
|||||||
RUN npm ci && npm run build
|
RUN npm ci && npm run build
|
||||||
|
|
||||||
# Stage 3: Poetry to requirements.txt export
|
# Stage 3: Poetry to requirements.txt export
|
||||||
FROM docker.io/python:3.11.2-slim-bullseye AS poetry-locker
|
FROM docker.io/python:3.11.1-slim-bullseye AS poetry-locker
|
||||||
|
|
||||||
WORKDIR /work
|
WORKDIR /work
|
||||||
COPY ./pyproject.toml /work
|
COPY ./pyproject.toml /work
|
||||||
@ -31,7 +31,7 @@ RUN pip install --no-cache-dir poetry && \
|
|||||||
poetry export -f requirements.txt --dev --output requirements-dev.txt
|
poetry export -f requirements.txt --dev --output requirements-dev.txt
|
||||||
|
|
||||||
# Stage 4: Build go proxy
|
# Stage 4: Build go proxy
|
||||||
FROM docker.io/golang:1.20.2-bullseye AS go-builder
|
FROM docker.io/golang:1.19.5-bullseye AS go-builder
|
||||||
|
|
||||||
WORKDIR /work
|
WORKDIR /work
|
||||||
|
|
||||||
@ -62,7 +62,7 @@ RUN --mount=type=secret,id=GEOIPUPDATE_ACCOUNT_ID \
|
|||||||
"
|
"
|
||||||
|
|
||||||
# Stage 6: Run
|
# Stage 6: Run
|
||||||
FROM docker.io/python:3.11.2-slim-bullseye AS final-image
|
FROM docker.io/python:3.11.1-slim-bullseye AS final-image
|
||||||
|
|
||||||
LABEL org.opencontainers.image.url https://goauthentik.io
|
LABEL org.opencontainers.image.url https://goauthentik.io
|
||||||
LABEL org.opencontainers.image.description goauthentik.io Main server image, see https://goauthentik.io for more info.
|
LABEL org.opencontainers.image.description goauthentik.io Main server image, see https://goauthentik.io for more info.
|
||||||
@ -96,7 +96,7 @@ RUN apt-get update && \
|
|||||||
|
|
||||||
COPY ./authentik/ /authentik
|
COPY ./authentik/ /authentik
|
||||||
COPY ./pyproject.toml /
|
COPY ./pyproject.toml /
|
||||||
COPY ./schemas /schemas
|
COPY ./xml /xml
|
||||||
COPY ./locale /locale
|
COPY ./locale /locale
|
||||||
COPY ./tests /tests
|
COPY ./tests /tests
|
||||||
COPY ./manage.py /
|
COPY ./manage.py /
|
||||||
|
12
README.md
12
README.md
@ -38,10 +38,6 @@ See [Development Documentation](https://goauthentik.io/developer-docs/?utm_sourc
|
|||||||
|
|
||||||
See [SECURITY.md](SECURITY.md)
|
See [SECURITY.md](SECURITY.md)
|
||||||
|
|
||||||
## Support
|
|
||||||
|
|
||||||
Your organization uses authentik? We'd love to add your logo to the readme and our website! Email us @ hello@goauthentik.io or open a GitHub Issue/PR!
|
|
||||||
|
|
||||||
## Sponsors
|
## Sponsors
|
||||||
|
|
||||||
This project is proudly sponsored by:
|
This project is proudly sponsored by:
|
||||||
@ -53,3 +49,11 @@ This project is proudly sponsored by:
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
DigitalOcean provides development and testing resources for authentik.
|
DigitalOcean provides development and testing resources for authentik.
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<a href="https://www.netlify.com">
|
||||||
|
<img src="https://www.netlify.com/img/global/badges/netlify-color-accent.svg" alt="Deploys by Netlify" />
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
Netlify hosts the [goauthentik.io](https://goauthentik.io) site.
|
||||||
|
@ -8,7 +8,6 @@ Authentik takes security very seriously. We follow the rules of [responsible dis
|
|||||||
| --------- | ------------------ |
|
| --------- | ------------------ |
|
||||||
| 2022.12.x | :white_check_mark: |
|
| 2022.12.x | :white_check_mark: |
|
||||||
| 2023.1.x | :white_check_mark: |
|
| 2023.1.x | :white_check_mark: |
|
||||||
| 2023.2.x | :white_check_mark: |
|
|
||||||
|
|
||||||
## Reporting a Vulnerability
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
from os import environ
|
from os import environ
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
__version__ = "2023.3.0"
|
__version__ = "2023.1.2"
|
||||||
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
||||||
|
|
||||||
|
|
||||||
|
@ -97,14 +97,8 @@ class SystemView(APIView):
|
|||||||
permission_classes = [IsAdminUser]
|
permission_classes = [IsAdminUser]
|
||||||
pagination_class = None
|
pagination_class = None
|
||||||
filter_backends = []
|
filter_backends = []
|
||||||
serializer_class = SystemSerializer
|
|
||||||
|
|
||||||
@extend_schema(responses={200: SystemSerializer(many=False)})
|
@extend_schema(responses={200: SystemSerializer(many=False)})
|
||||||
def get(self, request: Request) -> Response:
|
def get(self, request: Request) -> Response:
|
||||||
"""Get system information."""
|
"""Get system information."""
|
||||||
return Response(SystemSerializer(request).data)
|
return Response(SystemSerializer(request).data)
|
||||||
|
|
||||||
@extend_schema(responses={200: SystemSerializer(many=False)})
|
|
||||||
def post(self, request: Request) -> Response:
|
|
||||||
"""Get system information."""
|
|
||||||
return Response(SystemSerializer(request).data)
|
|
||||||
|
@ -50,8 +50,7 @@ class TaskSerializer(PassiveSerializer):
|
|||||||
are pickled in cache. In that case, just delete the info"""
|
are pickled in cache. In that case, just delete the info"""
|
||||||
try:
|
try:
|
||||||
return super().to_representation(instance)
|
return super().to_representation(instance)
|
||||||
# pylint: disable=broad-except
|
except AttributeError: # pragma: no cover
|
||||||
except Exception: # pragma: no cover
|
|
||||||
if isinstance(self.instance, list):
|
if isinstance(self.instance, list):
|
||||||
for inst in self.instance:
|
for inst in self.instance:
|
||||||
inst.delete()
|
inst.delete()
|
||||||
|
@ -18,4 +18,4 @@ def monitoring_set_workers(sender, **kwargs):
|
|||||||
def monitoring_set_tasks(sender, **kwargs):
|
def monitoring_set_tasks(sender, **kwargs):
|
||||||
"""Set task gauges"""
|
"""Set task gauges"""
|
||||||
for task in TaskInfo.all().values():
|
for task in TaskInfo.all().values():
|
||||||
task.update_metrics()
|
task.set_prom_metrics()
|
||||||
|
@ -9,7 +9,6 @@ from authentik.blueprints.tests import reconcile_app
|
|||||||
from authentik.core.models import Group, User
|
from authentik.core.models import Group, User
|
||||||
from authentik.core.tasks import clean_expired_models
|
from authentik.core.tasks import clean_expired_models
|
||||||
from authentik.events.monitored_tasks import TaskResultStatus
|
from authentik.events.monitored_tasks import TaskResultStatus
|
||||||
from authentik.lib.generators import generate_id
|
|
||||||
|
|
||||||
|
|
||||||
class TestAdminAPI(TestCase):
|
class TestAdminAPI(TestCase):
|
||||||
@ -17,8 +16,8 @@ class TestAdminAPI(TestCase):
|
|||||||
|
|
||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.user = User.objects.create(username=generate_id())
|
self.user = User.objects.create(username="test-user")
|
||||||
self.group = Group.objects.create(name=generate_id(), is_superuser=True)
|
self.group = Group.objects.create(name="superusers", is_superuser=True)
|
||||||
self.group.users.add(self.user)
|
self.group.users.add(self.user)
|
||||||
self.group.save()
|
self.group.save()
|
||||||
self.client.force_login(self.user)
|
self.client.force_login(self.user)
|
||||||
|
@ -42,7 +42,7 @@ def bearer_auth(raw_header: bytes) -> Optional[User]:
|
|||||||
|
|
||||||
def auth_user_lookup(raw_header: bytes) -> Optional[User]:
|
def auth_user_lookup(raw_header: bytes) -> Optional[User]:
|
||||||
"""raw_header in the Format of `Bearer ....`"""
|
"""raw_header in the Format of `Bearer ....`"""
|
||||||
from authentik.providers.oauth2.models import AccessToken
|
from authentik.providers.oauth2.models import RefreshToken
|
||||||
|
|
||||||
auth_credentials = validate_auth(raw_header)
|
auth_credentials = validate_auth(raw_header)
|
||||||
if not auth_credentials:
|
if not auth_credentials:
|
||||||
@ -55,8 +55,8 @@ def auth_user_lookup(raw_header: bytes) -> Optional[User]:
|
|||||||
CTX_AUTH_VIA.set("api_token")
|
CTX_AUTH_VIA.set("api_token")
|
||||||
return key_token.user
|
return key_token.user
|
||||||
# then try to auth via JWT
|
# then try to auth via JWT
|
||||||
jwt_token = AccessToken.filter_not_expired(
|
jwt_token = RefreshToken.filter_not_expired(
|
||||||
token=auth_credentials, _scope__icontains=SCOPE_AUTHENTIK_API
|
refresh_token=auth_credentials, _scope__icontains=SCOPE_AUTHENTIK_API
|
||||||
).first()
|
).first()
|
||||||
if jwt_token:
|
if jwt_token:
|
||||||
# Double-check scopes, since they are saved in a single string
|
# Double-check scopes, since they are saved in a single string
|
||||||
|
@ -1,10 +1,8 @@
|
|||||||
"""Test API Authentication"""
|
"""Test API Authentication"""
|
||||||
import json
|
|
||||||
from base64 import b64encode
|
from base64 import b64encode
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.utils import timezone
|
|
||||||
from rest_framework.exceptions import AuthenticationFailed
|
from rest_framework.exceptions import AuthenticationFailed
|
||||||
|
|
||||||
from authentik.api.authentication import bearer_auth
|
from authentik.api.authentication import bearer_auth
|
||||||
@ -13,7 +11,7 @@ from authentik.core.models import USER_ATTRIBUTE_SA, Token, TokenIntents
|
|||||||
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
|
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
|
||||||
from authentik.lib.generators import generate_id
|
from authentik.lib.generators import generate_id
|
||||||
from authentik.providers.oauth2.constants import SCOPE_AUTHENTIK_API
|
from authentik.providers.oauth2.constants import SCOPE_AUTHENTIK_API
|
||||||
from authentik.providers.oauth2.models import AccessToken, OAuth2Provider
|
from authentik.providers.oauth2.models import OAuth2Provider, RefreshToken
|
||||||
|
|
||||||
|
|
||||||
class TestAPIAuth(TestCase):
|
class TestAPIAuth(TestCase):
|
||||||
@ -65,28 +63,24 @@ class TestAPIAuth(TestCase):
|
|||||||
provider = OAuth2Provider.objects.create(
|
provider = OAuth2Provider.objects.create(
|
||||||
name=generate_id(), client_id=generate_id(), authorization_flow=create_test_flow()
|
name=generate_id(), client_id=generate_id(), authorization_flow=create_test_flow()
|
||||||
)
|
)
|
||||||
refresh = AccessToken.objects.create(
|
refresh = RefreshToken.objects.create(
|
||||||
user=create_test_admin_user(),
|
user=create_test_admin_user(),
|
||||||
provider=provider,
|
provider=provider,
|
||||||
token=generate_id(),
|
refresh_token=generate_id(),
|
||||||
auth_time=timezone.now(),
|
|
||||||
_scope=SCOPE_AUTHENTIK_API,
|
_scope=SCOPE_AUTHENTIK_API,
|
||||||
_id_token=json.dumps({}),
|
|
||||||
)
|
)
|
||||||
self.assertEqual(bearer_auth(f"Bearer {refresh.token}".encode()), refresh.user)
|
self.assertEqual(bearer_auth(f"Bearer {refresh.refresh_token}".encode()), refresh.user)
|
||||||
|
|
||||||
def test_jwt_missing_scope(self):
|
def test_jwt_missing_scope(self):
|
||||||
"""Test valid JWT"""
|
"""Test valid JWT"""
|
||||||
provider = OAuth2Provider.objects.create(
|
provider = OAuth2Provider.objects.create(
|
||||||
name=generate_id(), client_id=generate_id(), authorization_flow=create_test_flow()
|
name=generate_id(), client_id=generate_id(), authorization_flow=create_test_flow()
|
||||||
)
|
)
|
||||||
refresh = AccessToken.objects.create(
|
refresh = RefreshToken.objects.create(
|
||||||
user=create_test_admin_user(),
|
user=create_test_admin_user(),
|
||||||
provider=provider,
|
provider=provider,
|
||||||
token=generate_id(),
|
refresh_token=generate_id(),
|
||||||
auth_time=timezone.now(),
|
|
||||||
_scope="",
|
_scope="",
|
||||||
_id_token=json.dumps({}),
|
|
||||||
)
|
)
|
||||||
with self.assertRaises(AuthenticationFailed):
|
with self.assertRaises(AuthenticationFailed):
|
||||||
self.assertEqual(bearer_auth(f"Bearer {refresh.token}".encode()), refresh.user)
|
self.assertEqual(bearer_auth(f"Bearer {refresh.refresh_token}".encode()), refresh.user)
|
||||||
|
@ -4,7 +4,6 @@ from guardian.shortcuts import assign_perm
|
|||||||
from rest_framework.test import APITestCase
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
from authentik.core.models import Application, User
|
from authentik.core.models import Application, User
|
||||||
from authentik.lib.generators import generate_id
|
|
||||||
|
|
||||||
|
|
||||||
class TestAPIDecorators(APITestCase):
|
class TestAPIDecorators(APITestCase):
|
||||||
@ -17,7 +16,7 @@ class TestAPIDecorators(APITestCase):
|
|||||||
def test_obj_perm_denied(self):
|
def test_obj_perm_denied(self):
|
||||||
"""Test object perm denied"""
|
"""Test object perm denied"""
|
||||||
self.client.force_login(self.user)
|
self.client.force_login(self.user)
|
||||||
app = Application.objects.create(name=generate_id(), slug=generate_id())
|
app = Application.objects.create(name="denied", slug="denied")
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
reverse("authentik_api:application-metrics", kwargs={"slug": app.slug})
|
reverse("authentik_api:application-metrics", kwargs={"slug": app.slug})
|
||||||
)
|
)
|
||||||
@ -26,7 +25,7 @@ class TestAPIDecorators(APITestCase):
|
|||||||
def test_other_perm_denied(self):
|
def test_other_perm_denied(self):
|
||||||
"""Test other perm denied"""
|
"""Test other perm denied"""
|
||||||
self.client.force_login(self.user)
|
self.client.force_login(self.user)
|
||||||
app = Application.objects.create(name=generate_id(), slug=generate_id())
|
app = Application.objects.create(name="denied", slug="denied")
|
||||||
assign_perm("authentik_core.view_application", self.user, app)
|
assign_perm("authentik_core.view_application", self.user, app)
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
reverse("authentik_api:application-metrics", kwargs={"slug": app.slug})
|
reverse("authentik_api:application-metrics", kwargs={"slug": app.slug})
|
||||||
|
@ -50,16 +50,10 @@ from authentik.policies.reputation.api import ReputationPolicyViewSet, Reputatio
|
|||||||
from authentik.providers.ldap.api import LDAPOutpostConfigViewSet, LDAPProviderViewSet
|
from authentik.providers.ldap.api import LDAPOutpostConfigViewSet, LDAPProviderViewSet
|
||||||
from authentik.providers.oauth2.api.providers import OAuth2ProviderViewSet
|
from authentik.providers.oauth2.api.providers import OAuth2ProviderViewSet
|
||||||
from authentik.providers.oauth2.api.scopes import ScopeMappingViewSet
|
from authentik.providers.oauth2.api.scopes import ScopeMappingViewSet
|
||||||
from authentik.providers.oauth2.api.tokens import (
|
from authentik.providers.oauth2.api.tokens import AuthorizationCodeViewSet, RefreshTokenViewSet
|
||||||
AccessTokenViewSet,
|
|
||||||
AuthorizationCodeViewSet,
|
|
||||||
RefreshTokenViewSet,
|
|
||||||
)
|
|
||||||
from authentik.providers.proxy.api import ProxyOutpostConfigViewSet, ProxyProviderViewSet
|
from authentik.providers.proxy.api import ProxyOutpostConfigViewSet, ProxyProviderViewSet
|
||||||
from authentik.providers.saml.api.property_mapping import SAMLPropertyMappingViewSet
|
from authentik.providers.saml.api.property_mapping import SAMLPropertyMappingViewSet
|
||||||
from authentik.providers.saml.api.providers import SAMLProviderViewSet
|
from authentik.providers.saml.api.providers import SAMLProviderViewSet
|
||||||
from authentik.providers.scim.api.property_mapping import SCIMMappingViewSet
|
|
||||||
from authentik.providers.scim.api.providers import SCIMProviderViewSet
|
|
||||||
from authentik.sources.ldap.api import LDAPPropertyMappingViewSet, LDAPSourceViewSet
|
from authentik.sources.ldap.api import LDAPPropertyMappingViewSet, LDAPSourceViewSet
|
||||||
from authentik.sources.oauth.api.source import OAuthSourceViewSet
|
from authentik.sources.oauth.api.source import OAuthSourceViewSet
|
||||||
from authentik.sources.oauth.api.source_connection import UserOAuthSourceConnectionViewSet
|
from authentik.sources.oauth.api.source_connection import UserOAuthSourceConnectionViewSet
|
||||||
@ -165,18 +159,15 @@ router.register("providers/ldap", LDAPProviderViewSet)
|
|||||||
router.register("providers/proxy", ProxyProviderViewSet)
|
router.register("providers/proxy", ProxyProviderViewSet)
|
||||||
router.register("providers/oauth2", OAuth2ProviderViewSet)
|
router.register("providers/oauth2", OAuth2ProviderViewSet)
|
||||||
router.register("providers/saml", SAMLProviderViewSet)
|
router.register("providers/saml", SAMLProviderViewSet)
|
||||||
router.register("providers/scim", SCIMProviderViewSet)
|
|
||||||
|
|
||||||
router.register("oauth2/authorization_codes", AuthorizationCodeViewSet)
|
router.register("oauth2/authorization_codes", AuthorizationCodeViewSet)
|
||||||
router.register("oauth2/refresh_tokens", RefreshTokenViewSet)
|
router.register("oauth2/refresh_tokens", RefreshTokenViewSet)
|
||||||
router.register("oauth2/access_tokens", AccessTokenViewSet)
|
|
||||||
|
|
||||||
router.register("propertymappings/all", PropertyMappingViewSet)
|
router.register("propertymappings/all", PropertyMappingViewSet)
|
||||||
router.register("propertymappings/ldap", LDAPPropertyMappingViewSet)
|
router.register("propertymappings/ldap", LDAPPropertyMappingViewSet)
|
||||||
router.register("propertymappings/saml", SAMLPropertyMappingViewSet)
|
router.register("propertymappings/saml", SAMLPropertyMappingViewSet)
|
||||||
router.register("propertymappings/scope", ScopeMappingViewSet)
|
router.register("propertymappings/scope", ScopeMappingViewSet)
|
||||||
router.register("propertymappings/notification", NotificationWebhookMappingViewSet)
|
router.register("propertymappings/notification", NotificationWebhookMappingViewSet)
|
||||||
router.register("propertymappings/scim", SCIMMappingViewSet)
|
|
||||||
|
|
||||||
router.register("authenticators/all", DeviceViewSet, basename="device")
|
router.register("authenticators/all", DeviceViewSet, basename="device")
|
||||||
router.register("authenticators/duo", DuoDeviceViewSet)
|
router.register("authenticators/duo", DuoDeviceViewSet)
|
||||||
|
@ -58,6 +58,7 @@ class BlueprintInstanceSerializer(ModelSerializer):
|
|||||||
return super().validate(attrs)
|
return super().validate(attrs)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
model = BlueprintInstance
|
model = BlueprintInstance
|
||||||
fields = [
|
fields = [
|
||||||
"pk",
|
"pk",
|
||||||
|
@ -71,6 +71,7 @@ def migration_blueprint_import(apps: Apps, schema_editor: BaseDatabaseSchemaEdit
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
initial = True
|
initial = True
|
||||||
|
|
||||||
dependencies = [("authentik_flows", "0001_initial")]
|
dependencies = [("authentik_flows", "0001_initial")]
|
||||||
@ -85,12 +86,7 @@ class Migration(migrations.Migration):
|
|||||||
"managed",
|
"managed",
|
||||||
models.TextField(
|
models.TextField(
|
||||||
default=None,
|
default=None,
|
||||||
help_text=(
|
help_text="Objects which are managed by authentik. These objects are created and updated automatically. This is flag only indicates that an object can be overwritten by migrations. You can still modify the objects via the API, but expect changes to be overwritten in a later update.",
|
||||||
"Objects which are managed by authentik. These objects are created and"
|
|
||||||
" updated automatically. This is flag only indicates that an object can"
|
|
||||||
" be overwritten by migrations. You can still modify the objects via"
|
|
||||||
" the API, but expect changes to be overwritten in a later update."
|
|
||||||
),
|
|
||||||
null=True,
|
null=True,
|
||||||
unique=True,
|
unique=True,
|
||||||
verbose_name="Managed by authentik",
|
verbose_name="Managed by authentik",
|
||||||
|
@ -4,6 +4,7 @@ from django.db import migrations, models
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("authentik_blueprints", "0001_initial"),
|
("authentik_blueprints", "0001_initial"),
|
||||||
]
|
]
|
||||||
|
@ -29,15 +29,18 @@ class ManagedModel(models.Model):
|
|||||||
null=True,
|
null=True,
|
||||||
verbose_name=_("Managed by authentik"),
|
verbose_name=_("Managed by authentik"),
|
||||||
help_text=_(
|
help_text=_(
|
||||||
"Objects which are managed by authentik. These objects are created and updated "
|
(
|
||||||
"automatically. This is flag only indicates that an object can be overwritten by "
|
"Objects which are managed by authentik. These objects are created and updated "
|
||||||
"migrations. You can still modify the objects via the API, but expect changes "
|
"automatically. This is flag only indicates that an object can be overwritten by "
|
||||||
"to be overwritten in a later update."
|
"migrations. You can still modify the objects via the API, but expect changes "
|
||||||
|
"to be overwritten in a later update."
|
||||||
|
)
|
||||||
),
|
),
|
||||||
unique=True,
|
unique=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
abstract = True
|
abstract = True
|
||||||
|
|
||||||
|
|
||||||
@ -106,6 +109,7 @@ class BlueprintInstance(SerializerModel, ManagedModel, CreatedUpdatedModel):
|
|||||||
return f"Blueprint Instance {self.name}"
|
return f"Blueprint Instance {self.name}"
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
verbose_name = _("Blueprint Instance")
|
verbose_name = _("Blueprint Instance")
|
||||||
verbose_name_plural = _("Blueprint Instances")
|
verbose_name_plural = _("Blueprint Instances")
|
||||||
unique_together = (
|
unique_together = (
|
||||||
|
@ -24,14 +24,18 @@ class TestBlueprintsV1(TransactionTestCase):
|
|||||||
importer = Importer('{"version": 3}')
|
importer = Importer('{"version": 3}')
|
||||||
self.assertFalse(importer.validate()[0])
|
self.assertFalse(importer.validate()[0])
|
||||||
importer = Importer(
|
importer = Importer(
|
||||||
'{"version": 1,"entries":[{"identifiers":{},"attrs":{},'
|
(
|
||||||
'"model": "authentik_core.User"}]}'
|
'{"version": 1,"entries":[{"identifiers":{},"attrs":{},'
|
||||||
|
'"model": "authentik_core.User"}]}'
|
||||||
|
)
|
||||||
)
|
)
|
||||||
self.assertFalse(importer.validate()[0])
|
self.assertFalse(importer.validate()[0])
|
||||||
importer = Importer(
|
importer = Importer(
|
||||||
'{"version": 1, "entries": [{"attrs": {"name": "test"}, '
|
(
|
||||||
'"identifiers": {}, '
|
'{"version": 1, "entries": [{"attrs": {"name": "test"}, '
|
||||||
'"model": "authentik_core.Group"}]}'
|
'"identifiers": {}, '
|
||||||
|
'"model": "authentik_core.Group"}]}'
|
||||||
|
)
|
||||||
)
|
)
|
||||||
self.assertFalse(importer.validate()[0])
|
self.assertFalse(importer.validate()[0])
|
||||||
|
|
||||||
@ -55,9 +59,11 @@ class TestBlueprintsV1(TransactionTestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
importer = Importer(
|
importer = Importer(
|
||||||
'{"version": 1, "entries": [{"attrs": {"name": "test999", "attributes": '
|
(
|
||||||
'{"key": ["updated_value"]}}, "identifiers": {"attributes": {"other_key": '
|
'{"version": 1, "entries": [{"attrs": {"name": "test999", "attributes": '
|
||||||
'["other_value"]}}, "model": "authentik_core.Group"}]}'
|
'{"key": ["updated_value"]}}, "identifiers": {"attributes": {"other_key": '
|
||||||
|
'["other_value"]}}, "model": "authentik_core.Group"}]}'
|
||||||
|
)
|
||||||
)
|
)
|
||||||
self.assertTrue(importer.validate()[0])
|
self.assertTrue(importer.validate()[0])
|
||||||
self.assertTrue(importer.apply())
|
self.assertTrue(importer.apply())
|
||||||
|
@ -7,7 +7,6 @@ from dacite.config import Config
|
|||||||
from dacite.core import from_dict
|
from dacite.core import from_dict
|
||||||
from dacite.exceptions import DaciteError
|
from dacite.exceptions import DaciteError
|
||||||
from deepmerge import always_merger
|
from deepmerge import always_merger
|
||||||
from django.core.exceptions import FieldError
|
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.db.models import Model
|
from django.db.models import Model
|
||||||
from django.db.models.query_utils import Q
|
from django.db.models.query_utils import Q
|
||||||
@ -182,10 +181,7 @@ class Importer:
|
|||||||
if not query:
|
if not query:
|
||||||
raise EntryInvalidError("No or invalid identifiers")
|
raise EntryInvalidError("No or invalid identifiers")
|
||||||
|
|
||||||
try:
|
existing_models = model.objects.filter(query)
|
||||||
existing_models = model.objects.filter(query)
|
|
||||||
except FieldError as exc:
|
|
||||||
raise EntryInvalidError(f"Invalid identifier field: {exc}") from exc
|
|
||||||
|
|
||||||
serializer_kwargs = {}
|
serializer_kwargs = {}
|
||||||
model_instance = existing_models.first()
|
model_instance = existing_models.first()
|
||||||
@ -235,7 +231,8 @@ class Importer:
|
|||||||
raise IntegrityError
|
raise IntegrityError
|
||||||
except IntegrityError:
|
except IntegrityError:
|
||||||
return False
|
return False
|
||||||
self.logger.debug("Committing changes")
|
else:
|
||||||
|
self.logger.debug("Committing changes")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _apply_models(self) -> bool:
|
def _apply_models(self) -> bool:
|
||||||
|
@ -56,4 +56,5 @@ class MetaApplyBlueprint(BaseMetaModel):
|
|||||||
return ApplyBlueprintMetaSerializer
|
return ApplyBlueprintMetaSerializer
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
abstract = True
|
abstract = True
|
||||||
|
@ -14,6 +14,7 @@ class BaseMetaModel(Model):
|
|||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
abstract = True
|
abstract = True
|
||||||
|
|
||||||
|
|
||||||
|
@ -37,6 +37,7 @@ from authentik.lib.utils.file import (
|
|||||||
from authentik.policies.api.exec import PolicyTestResultSerializer
|
from authentik.policies.api.exec import PolicyTestResultSerializer
|
||||||
from authentik.policies.engine import PolicyEngine
|
from authentik.policies.engine import PolicyEngine
|
||||||
from authentik.policies.types import PolicyResult
|
from authentik.policies.types import PolicyResult
|
||||||
|
from authentik.stages.user_login.stage import USER_LOGIN_AUTHENTICATED
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
|
||||||
@ -62,6 +63,7 @@ class ApplicationSerializer(ModelSerializer):
|
|||||||
return app.get_launch_url(user)
|
return app.get_launch_url(user)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
model = Application
|
model = Application
|
||||||
fields = [
|
fields = [
|
||||||
"pk",
|
"pk",
|
||||||
@ -185,6 +187,10 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet):
|
|||||||
if superuser_full_list and request.user.is_superuser:
|
if superuser_full_list and request.user.is_superuser:
|
||||||
return super().list(request)
|
return super().list(request)
|
||||||
|
|
||||||
|
# To prevent the user from having to double login when prompt is set to login
|
||||||
|
# and the user has just signed it. This session variable is set in the UserLoginStage
|
||||||
|
# and is (quite hackily) removed from the session in applications's API's List method
|
||||||
|
self.request.session.pop(USER_LOGIN_AUTHENTICATED, None)
|
||||||
queryset = self._filter_queryset_for_list(self.get_queryset())
|
queryset = self._filter_queryset_for_list(self.get_queryset())
|
||||||
self.paginate_queryset(queryset)
|
self.paginate_queryset(queryset)
|
||||||
|
|
||||||
|
@ -74,6 +74,7 @@ class AuthenticatedSessionSerializer(ModelSerializer):
|
|||||||
return GEOIP_READER.city_dict(instance.last_ip)
|
return GEOIP_READER.city_dict(instance.last_ip)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
model = AuthenticatedSession
|
model = AuthenticatedSession
|
||||||
fields = [
|
fields = [
|
||||||
"uuid",
|
"uuid",
|
||||||
|
@ -24,10 +24,12 @@ from authentik.core.models import Group, User
|
|||||||
class GroupMemberSerializer(ModelSerializer):
|
class GroupMemberSerializer(ModelSerializer):
|
||||||
"""Stripped down user serializer to show relevant users for groups"""
|
"""Stripped down user serializer to show relevant users for groups"""
|
||||||
|
|
||||||
|
avatar = CharField(read_only=True)
|
||||||
attributes = JSONField(validators=[is_dict], required=False)
|
attributes = JSONField(validators=[is_dict], required=False)
|
||||||
uid = CharField(read_only=True)
|
uid = CharField(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
model = User
|
model = User
|
||||||
fields = [
|
fields = [
|
||||||
"pk",
|
"pk",
|
||||||
@ -36,6 +38,7 @@ class GroupMemberSerializer(ModelSerializer):
|
|||||||
"is_active",
|
"is_active",
|
||||||
"last_login",
|
"last_login",
|
||||||
"email",
|
"email",
|
||||||
|
"avatar",
|
||||||
"attributes",
|
"attributes",
|
||||||
"uid",
|
"uid",
|
||||||
]
|
]
|
||||||
@ -53,6 +56,7 @@ class GroupSerializer(ModelSerializer):
|
|||||||
num_pk = IntegerField(read_only=True)
|
num_pk = IntegerField(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
model = Group
|
model = Group
|
||||||
fields = [
|
fields = [
|
||||||
"pk",
|
"pk",
|
||||||
@ -110,6 +114,7 @@ class GroupFilter(FilterSet):
|
|||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
model = Group
|
model = Group
|
||||||
fields = ["name", "is_superuser", "members_by_pk", "attributes", "members_by_username"]
|
fields = ["name", "is_superuser", "members_by_pk", "attributes", "members_by_username"]
|
||||||
|
|
||||||
|
@ -49,6 +49,7 @@ class PropertyMappingSerializer(ManagedSerializer, ModelSerializer, MetaNameSeri
|
|||||||
return expression
|
return expression
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
model = PropertyMapping
|
model = PropertyMapping
|
||||||
fields = [
|
fields = [
|
||||||
"pk",
|
"pk",
|
||||||
|
@ -31,6 +31,7 @@ class ProviderSerializer(ModelSerializer, MetaNameSerializer):
|
|||||||
return obj.component
|
return obj.component
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
model = Provider
|
model = Provider
|
||||||
fields = [
|
fields = [
|
||||||
"pk",
|
"pk",
|
||||||
@ -44,9 +45,6 @@ class ProviderSerializer(ModelSerializer, MetaNameSerializer):
|
|||||||
"verbose_name_plural",
|
"verbose_name_plural",
|
||||||
"meta_model_name",
|
"meta_model_name",
|
||||||
]
|
]
|
||||||
extra_kwargs = {
|
|
||||||
"authorization_flow": {"required": True, "allow_null": False},
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class ProviderViewSet(
|
class ProviderViewSet(
|
||||||
|
@ -46,6 +46,7 @@ class SourceSerializer(ModelSerializer, MetaNameSerializer):
|
|||||||
return obj.component
|
return obj.component
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
model = Source
|
model = Source
|
||||||
fields = [
|
fields = [
|
||||||
"pk",
|
"pk",
|
||||||
@ -206,6 +207,5 @@ class UserSourceConnectionViewSet(
|
|||||||
queryset = UserSourceConnection.objects.all()
|
queryset = UserSourceConnection.objects.all()
|
||||||
serializer_class = UserSourceConnectionSerializer
|
serializer_class = UserSourceConnectionSerializer
|
||||||
permission_classes = [OwnerSuperuserPermissions]
|
permission_classes = [OwnerSuperuserPermissions]
|
||||||
filterset_fields = ["user"]
|
|
||||||
filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter]
|
filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter]
|
||||||
ordering = ["pk"]
|
ordering = ["pk"]
|
||||||
|
@ -31,20 +31,15 @@ class TokenSerializer(ManagedSerializer, ModelSerializer):
|
|||||||
|
|
||||||
def validate(self, attrs: dict[Any, str]) -> dict[Any, str]:
|
def validate(self, attrs: dict[Any, str]) -> dict[Any, str]:
|
||||||
"""Ensure only API or App password tokens are created."""
|
"""Ensure only API or App password tokens are created."""
|
||||||
request: Request = self.context.get("request")
|
request: Request = self.context["request"]
|
||||||
if not request:
|
attrs.setdefault("user", request.user)
|
||||||
if "user" not in attrs:
|
|
||||||
raise ValidationError("Missing user")
|
|
||||||
if "intent" not in attrs:
|
|
||||||
raise ValidationError("Missing intent")
|
|
||||||
else:
|
|
||||||
attrs.setdefault("user", request.user)
|
|
||||||
attrs.setdefault("intent", TokenIntents.INTENT_API)
|
attrs.setdefault("intent", TokenIntents.INTENT_API)
|
||||||
if attrs.get("intent") not in [TokenIntents.INTENT_API, TokenIntents.INTENT_APP_PASSWORD]:
|
if attrs.get("intent") not in [TokenIntents.INTENT_API, TokenIntents.INTENT_APP_PASSWORD]:
|
||||||
raise ValidationError(f"Invalid intent {attrs.get('intent')}")
|
raise ValidationError(f"Invalid intent {attrs.get('intent')}")
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
model = Token
|
model = Token
|
||||||
fields = [
|
fields = [
|
||||||
"pk",
|
"pk",
|
||||||
@ -139,10 +134,9 @@ class TokenViewSet(UsedByMixin, ModelViewSet):
|
|||||||
)
|
)
|
||||||
@action(detail=True, pagination_class=None, filter_backends=[], methods=["POST"])
|
@action(detail=True, pagination_class=None, filter_backends=[], methods=["POST"])
|
||||||
def set_key(self, request: Request, identifier: str) -> Response:
|
def set_key(self, request: Request, identifier: str) -> Response:
|
||||||
"""Set token key. Action is logged as event. `authentik_core.set_token_key` permission
|
"""Return token key and log access"""
|
||||||
is required."""
|
|
||||||
token: Token = self.get_object()
|
token: Token = self.get_object()
|
||||||
key = request.data.get("key")
|
key = request.POST.get("key")
|
||||||
if not key:
|
if not key:
|
||||||
return Response(status=400)
|
return Response(status=400)
|
||||||
token.key = key
|
token.key = key
|
||||||
|
@ -38,13 +38,11 @@ from rest_framework.request import Request
|
|||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.serializers import (
|
from rest_framework.serializers import (
|
||||||
BooleanField,
|
BooleanField,
|
||||||
DateTimeField,
|
|
||||||
ListSerializer,
|
ListSerializer,
|
||||||
ModelSerializer,
|
ModelSerializer,
|
||||||
PrimaryKeyRelatedField,
|
PrimaryKeyRelatedField,
|
||||||
ValidationError,
|
ValidationError,
|
||||||
)
|
)
|
||||||
from rest_framework.validators import UniqueValidator
|
|
||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
from rest_framework_guardian.filters import ObjectPermissionsFilter
|
from rest_framework_guardian.filters import ObjectPermissionsFilter
|
||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
@ -68,7 +66,6 @@ from authentik.core.models import (
|
|||||||
User,
|
User,
|
||||||
)
|
)
|
||||||
from authentik.events.models import EventAction
|
from authentik.events.models import EventAction
|
||||||
from authentik.flows.exceptions import FlowNonApplicableException
|
|
||||||
from authentik.flows.models import FlowToken
|
from authentik.flows.models import FlowToken
|
||||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner
|
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner
|
||||||
from authentik.flows.views.executor import QS_KEY_TOKEN
|
from authentik.flows.views.executor import QS_KEY_TOKEN
|
||||||
@ -87,6 +84,7 @@ class UserGroupSerializer(ModelSerializer):
|
|||||||
parent_name = CharField(source="parent.name", read_only=True)
|
parent_name = CharField(source="parent.name", read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
model = Group
|
model = Group
|
||||||
fields = [
|
fields = [
|
||||||
"pk",
|
"pk",
|
||||||
@ -110,7 +108,7 @@ class UserSerializer(ModelSerializer):
|
|||||||
)
|
)
|
||||||
groups_obj = ListSerializer(child=UserGroupSerializer(), read_only=True, source="ak_groups")
|
groups_obj = ListSerializer(child=UserGroupSerializer(), read_only=True, source="ak_groups")
|
||||||
uid = CharField(read_only=True)
|
uid = CharField(read_only=True)
|
||||||
username = CharField(max_length=150, validators=[UniqueValidator(queryset=User.objects.all())])
|
username = CharField(max_length=150)
|
||||||
|
|
||||||
def validate_path(self, path: str) -> str:
|
def validate_path(self, path: str) -> str:
|
||||||
"""Validate path"""
|
"""Validate path"""
|
||||||
@ -122,6 +120,7 @@ class UserSerializer(ModelSerializer):
|
|||||||
return path
|
return path
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
model = User
|
model = User
|
||||||
fields = [
|
fields = [
|
||||||
"pk",
|
"pk",
|
||||||
@ -173,6 +172,7 @@ class UserSelfSerializer(ModelSerializer):
|
|||||||
return user.group_attributes(self._context["request"]).get("settings", {})
|
return user.group_attributes(self._context["request"]).get("settings", {})
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
model = User
|
model = User
|
||||||
fields = [
|
fields = [
|
||||||
"pk",
|
"pk",
|
||||||
@ -327,16 +327,12 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
|||||||
user: User = self.get_object()
|
user: User = self.get_object()
|
||||||
planner = FlowPlanner(flow)
|
planner = FlowPlanner(flow)
|
||||||
planner.allow_empty_flows = True
|
planner.allow_empty_flows = True
|
||||||
try:
|
plan = planner.plan(
|
||||||
plan = planner.plan(
|
self.request._request,
|
||||||
self.request._request,
|
{
|
||||||
{
|
PLAN_CONTEXT_PENDING_USER: user,
|
||||||
PLAN_CONTEXT_PENDING_USER: user,
|
},
|
||||||
},
|
)
|
||||||
)
|
|
||||||
except FlowNonApplicableException:
|
|
||||||
LOGGER.warning("Recovery flow not applicable to user")
|
|
||||||
return None, None
|
|
||||||
token, __ = FlowToken.objects.update_or_create(
|
token, __ = FlowToken.objects.update_or_create(
|
||||||
identifier=f"{user.uid}-password-reset",
|
identifier=f"{user.uid}-password-reset",
|
||||||
defaults={
|
defaults={
|
||||||
@ -359,11 +355,6 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
|||||||
{
|
{
|
||||||
"name": CharField(required=True),
|
"name": CharField(required=True),
|
||||||
"create_group": BooleanField(default=False),
|
"create_group": BooleanField(default=False),
|
||||||
"expiring": BooleanField(default=True),
|
|
||||||
"expires": DateTimeField(
|
|
||||||
required=False,
|
|
||||||
help_text="If not provided, valid for 360 days",
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
responses={
|
responses={
|
||||||
@ -384,20 +375,14 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
|||||||
"""Create a new user account that is marked as a service account"""
|
"""Create a new user account that is marked as a service account"""
|
||||||
username = request.data.get("name")
|
username = request.data.get("name")
|
||||||
create_group = request.data.get("create_group", False)
|
create_group = request.data.get("create_group", False)
|
||||||
expiring = request.data.get("expiring", True)
|
|
||||||
expires = request.data.get("expires", now() + timedelta(days=360))
|
|
||||||
|
|
||||||
with atomic():
|
with atomic():
|
||||||
try:
|
try:
|
||||||
user: User = User.objects.create(
|
user = User.objects.create(
|
||||||
username=username,
|
username=username,
|
||||||
name=username,
|
name=username,
|
||||||
attributes={USER_ATTRIBUTE_SA: True, USER_ATTRIBUTE_TOKEN_EXPIRING: expiring},
|
attributes={USER_ATTRIBUTE_SA: True, USER_ATTRIBUTE_TOKEN_EXPIRING: False},
|
||||||
path=USER_PATH_SERVICE_ACCOUNT,
|
path=USER_PATH_SERVICE_ACCOUNT,
|
||||||
)
|
)
|
||||||
user.set_unusable_password()
|
|
||||||
user.save()
|
|
||||||
|
|
||||||
response = {
|
response = {
|
||||||
"username": user.username,
|
"username": user.username,
|
||||||
"user_uid": user.uid,
|
"user_uid": user.uid,
|
||||||
@ -413,12 +398,11 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
|||||||
identifier=slugify(f"service-account-{username}-password"),
|
identifier=slugify(f"service-account-{username}-password"),
|
||||||
intent=TokenIntents.INTENT_APP_PASSWORD,
|
intent=TokenIntents.INTENT_APP_PASSWORD,
|
||||||
user=user,
|
user=user,
|
||||||
expires=expires,
|
expires=now() + timedelta(days=360),
|
||||||
expiring=expiring,
|
|
||||||
)
|
)
|
||||||
response["token"] = token.key
|
response["token"] = token.key
|
||||||
return Response(response)
|
return Response(response)
|
||||||
except IntegrityError as exc:
|
except (IntegrityError) as exc:
|
||||||
return Response(data={"non_field_errors": [str(exc)]}, status=400)
|
return Response(data={"non_field_errors": [str(exc)]}, status=400)
|
||||||
|
|
||||||
@extend_schema(responses={200: SessionUserSerializer(many=False)})
|
@extend_schema(responses={200: SessionUserSerializer(many=False)})
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
"""Property Mapping Evaluator"""
|
"""Property Mapping Evaluator"""
|
||||||
from typing import Any, Optional
|
from typing import Optional
|
||||||
|
|
||||||
from django.db.models import Model
|
from django.db.models import Model
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
from prometheus_client import Histogram
|
|
||||||
|
|
||||||
from authentik.core.models import User
|
from authentik.core.models import User
|
||||||
from authentik.events.models import Event, EventAction
|
from authentik.events.models import Event, EventAction
|
||||||
@ -11,12 +10,6 @@ from authentik.lib.expression.evaluator import BaseEvaluator
|
|||||||
from authentik.lib.utils.errors import exception_to_string
|
from authentik.lib.utils.errors import exception_to_string
|
||||||
from authentik.policies.types import PolicyRequest
|
from authentik.policies.types import PolicyRequest
|
||||||
|
|
||||||
PROPERTY_MAPPING_TIME = Histogram(
|
|
||||||
"authentik_property_mapping_execution_time",
|
|
||||||
"Evaluation time of property mappings",
|
|
||||||
["mapping_name"],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class PropertyMappingEvaluator(BaseEvaluator):
|
class PropertyMappingEvaluator(BaseEvaluator):
|
||||||
"""Custom Evaluator that adds some different context variables."""
|
"""Custom Evaluator that adds some different context variables."""
|
||||||
@ -56,7 +49,3 @@ class PropertyMappingEvaluator(BaseEvaluator):
|
|||||||
event.from_http(req.http_request, req.user)
|
event.from_http(req.http_request, req.user)
|
||||||
return
|
return
|
||||||
event.save()
|
event.save()
|
||||||
|
|
||||||
def evaluate(self, *args, **kwargs) -> Any:
|
|
||||||
with PROPERTY_MAPPING_TIME.labels(mapping_name=self._filename).time():
|
|
||||||
return super().evaluate(*args, **kwargs)
|
|
||||||
|
@ -14,6 +14,7 @@ import authentik.core.models
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
initial = True
|
initial = True
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
@ -43,10 +44,7 @@ class Migration(migrations.Migration):
|
|||||||
"is_superuser",
|
"is_superuser",
|
||||||
models.BooleanField(
|
models.BooleanField(
|
||||||
default=False,
|
default=False,
|
||||||
help_text=(
|
help_text="Designates that this user has all permissions without explicitly assigning them.",
|
||||||
"Designates that this user has all permissions without explicitly"
|
|
||||||
" assigning them."
|
|
||||||
),
|
|
||||||
verbose_name="superuser status",
|
verbose_name="superuser status",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -54,9 +52,7 @@ class Migration(migrations.Migration):
|
|||||||
"username",
|
"username",
|
||||||
models.CharField(
|
models.CharField(
|
||||||
error_messages={"unique": "A user with that username already exists."},
|
error_messages={"unique": "A user with that username already exists."},
|
||||||
help_text=(
|
help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.",
|
||||||
"Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only."
|
|
||||||
),
|
|
||||||
max_length=150,
|
max_length=150,
|
||||||
unique=True,
|
unique=True,
|
||||||
validators=[django.contrib.auth.validators.UnicodeUsernameValidator()],
|
validators=[django.contrib.auth.validators.UnicodeUsernameValidator()],
|
||||||
@ -87,10 +83,7 @@ class Migration(migrations.Migration):
|
|||||||
"is_active",
|
"is_active",
|
||||||
models.BooleanField(
|
models.BooleanField(
|
||||||
default=True,
|
default=True,
|
||||||
help_text=(
|
help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.",
|
||||||
"Designates whether this user should be treated as active. Unselect"
|
|
||||||
" this instead of deleting accounts."
|
|
||||||
),
|
|
||||||
verbose_name="active",
|
verbose_name="active",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -18,13 +18,13 @@ def create_default_user(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
|||||||
db_alias = schema_editor.connection.alias
|
db_alias = schema_editor.connection.alias
|
||||||
|
|
||||||
akadmin, _ = User.objects.using(db_alias).get_or_create(
|
akadmin, _ = User.objects.using(db_alias).get_or_create(
|
||||||
username="akadmin",
|
username="akadmin", email="root@localhost", name="authentik Default Admin"
|
||||||
email=environ.get("AUTHENTIK_BOOTSTRAP_EMAIL", "root@localhost"),
|
|
||||||
name="authentik Default Admin",
|
|
||||||
)
|
)
|
||||||
password = None
|
password = None
|
||||||
if "TF_BUILD" in environ or settings.TEST:
|
if "TF_BUILD" in environ or settings.TEST:
|
||||||
password = "akadmin" # noqa # nosec
|
password = "akadmin" # noqa # nosec
|
||||||
|
if "AK_ADMIN_PASS" in environ:
|
||||||
|
password = environ["AK_ADMIN_PASS"]
|
||||||
if "AUTHENTIK_BOOTSTRAP_PASSWORD" in environ:
|
if "AUTHENTIK_BOOTSTRAP_PASSWORD" in environ:
|
||||||
password = environ["AUTHENTIK_BOOTSTRAP_PASSWORD"]
|
password = environ["AUTHENTIK_BOOTSTRAP_PASSWORD"]
|
||||||
if password:
|
if password:
|
||||||
@ -51,6 +51,7 @@ def create_default_admin_group(apps: Apps, schema_editor: BaseDatabaseSchemaEdit
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
replaces = [
|
replaces = [
|
||||||
("authentik_core", "0002_auto_20200523_1133"),
|
("authentik_core", "0002_auto_20200523_1133"),
|
||||||
("authentik_core", "0003_default_user"),
|
("authentik_core", "0003_default_user"),
|
||||||
@ -171,10 +172,7 @@ class Migration(migrations.Migration):
|
|||||||
name="groups",
|
name="groups",
|
||||||
field=models.ManyToManyField(
|
field=models.ManyToManyField(
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text=(
|
help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.",
|
||||||
"The groups this user belongs to. A user will get all permissions granted to"
|
|
||||||
" each of their groups."
|
|
||||||
),
|
|
||||||
related_name="user_set",
|
related_name="user_set",
|
||||||
related_query_name="user",
|
related_query_name="user",
|
||||||
to="auth.Group",
|
to="auth.Group",
|
||||||
|
@ -17,6 +17,7 @@ def set_default_token_key(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
replaces = [
|
replaces = [
|
||||||
("authentik_core", "0012_auto_20201003_1737"),
|
("authentik_core", "0012_auto_20201003_1737"),
|
||||||
("authentik_core", "0013_auto_20201003_2132"),
|
("authentik_core", "0013_auto_20201003_2132"),
|
||||||
|
@ -4,6 +4,7 @@ from django.db import migrations, models
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("authentik_core", "0016_auto_20201202_2234"),
|
("authentik_core", "0016_auto_20201202_2234"),
|
||||||
]
|
]
|
||||||
@ -14,12 +15,7 @@ class Migration(migrations.Migration):
|
|||||||
name="managed",
|
name="managed",
|
||||||
field=models.TextField(
|
field=models.TextField(
|
||||||
default=None,
|
default=None,
|
||||||
help_text=(
|
help_text="Objects which are managed by authentik. These objects are created and updated automatically. This is flag only indicates that an object can be overwritten by migrations. You can still modify the objects via the API, but expect changes to be overwritten in a later update.",
|
||||||
"Objects which are managed by authentik. These objects are created and updated"
|
|
||||||
" automatically. This is flag only indicates that an object can be overwritten"
|
|
||||||
" by migrations. You can still modify the objects via the API, but expect"
|
|
||||||
" changes to be overwritten in a later update."
|
|
||||||
),
|
|
||||||
null=True,
|
null=True,
|
||||||
verbose_name="Managed by authentik",
|
verbose_name="Managed by authentik",
|
||||||
unique=True,
|
unique=True,
|
||||||
@ -30,12 +26,7 @@ class Migration(migrations.Migration):
|
|||||||
name="managed",
|
name="managed",
|
||||||
field=models.TextField(
|
field=models.TextField(
|
||||||
default=None,
|
default=None,
|
||||||
help_text=(
|
help_text="Objects which are managed by authentik. These objects are created and updated automatically. This is flag only indicates that an object can be overwritten by migrations. You can still modify the objects via the API, but expect changes to be overwritten in a later update.",
|
||||||
"Objects which are managed by authentik. These objects are created and updated"
|
|
||||||
" automatically. This is flag only indicates that an object can be overwritten"
|
|
||||||
" by migrations. You can still modify the objects via the API, but expect"
|
|
||||||
" changes to be overwritten in a later update."
|
|
||||||
),
|
|
||||||
null=True,
|
null=True,
|
||||||
verbose_name="Managed by authentik",
|
verbose_name="Managed by authentik",
|
||||||
unique=True,
|
unique=True,
|
||||||
|
@ -46,9 +46,13 @@ def create_default_user_token(apps: Apps, schema_editor: BaseDatabaseSchemaEdito
|
|||||||
akadmin = User.objects.using(db_alias).filter(username="akadmin")
|
akadmin = User.objects.using(db_alias).filter(username="akadmin")
|
||||||
if not akadmin.exists():
|
if not akadmin.exists():
|
||||||
return
|
return
|
||||||
if "AUTHENTIK_BOOTSTRAP_TOKEN" not in environ:
|
key = None
|
||||||
|
if "AK_ADMIN_TOKEN" in environ:
|
||||||
|
key = environ["AK_ADMIN_TOKEN"]
|
||||||
|
if "AUTHENTIK_BOOTSTRAP_TOKEN" in environ:
|
||||||
|
key = environ["AUTHENTIK_BOOTSTRAP_TOKEN"]
|
||||||
|
if not key:
|
||||||
return
|
return
|
||||||
key = environ["AUTHENTIK_BOOTSTRAP_TOKEN"]
|
|
||||||
Token.objects.using(db_alias).create(
|
Token.objects.using(db_alias).create(
|
||||||
identifier="authentik-bootstrap-token",
|
identifier="authentik-bootstrap-token",
|
||||||
user=akadmin.first(),
|
user=akadmin.first(),
|
||||||
@ -59,6 +63,7 @@ def create_default_user_token(apps: Apps, schema_editor: BaseDatabaseSchemaEdito
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
replaces = [
|
replaces = [
|
||||||
("authentik_core", "0018_auto_20210330_1345"),
|
("authentik_core", "0018_auto_20210330_1345"),
|
||||||
("authentik_core", "0019_source_managed"),
|
("authentik_core", "0019_source_managed"),
|
||||||
@ -91,12 +96,7 @@ class Migration(migrations.Migration):
|
|||||||
name="managed",
|
name="managed",
|
||||||
field=models.TextField(
|
field=models.TextField(
|
||||||
default=None,
|
default=None,
|
||||||
help_text=(
|
help_text="Objects which are managed by authentik. These objects are created and updated automatically. This is flag only indicates that an object can be overwritten by migrations. You can still modify the objects via the API, but expect changes to be overwritten in a later update.",
|
||||||
"Objects which are managed by authentik. These objects are created and updated"
|
|
||||||
" automatically. This is flag only indicates that an object can be overwritten"
|
|
||||||
" by migrations. You can still modify the objects via the API, but expect"
|
|
||||||
" changes to be overwritten in a later update."
|
|
||||||
),
|
|
||||||
null=True,
|
null=True,
|
||||||
unique=True,
|
unique=True,
|
||||||
verbose_name="Managed by authentik",
|
verbose_name="Managed by authentik",
|
||||||
@ -110,38 +110,23 @@ class Migration(migrations.Migration):
|
|||||||
("identifier", "Use the source-specific identifier"),
|
("identifier", "Use the source-specific identifier"),
|
||||||
(
|
(
|
||||||
"email_link",
|
"email_link",
|
||||||
(
|
"Link to a user with identical email address. Can have security implications when a source doesn't validate email addresses.",
|
||||||
"Link to a user with identical email address. Can have security"
|
|
||||||
" implications when a source doesn't validate email addresses."
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"email_deny",
|
"email_deny",
|
||||||
(
|
"Use the user's email address, but deny enrollment when the email address already exists.",
|
||||||
"Use the user's email address, but deny enrollment when the email"
|
|
||||||
" address already exists."
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"username_link",
|
"username_link",
|
||||||
(
|
"Link to a user with identical username. Can have security implications when a username is used with another source.",
|
||||||
"Link to a user with identical username. Can have security implications"
|
|
||||||
" when a username is used with another source."
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"username_deny",
|
"username_deny",
|
||||||
(
|
"Use the user's username, but deny enrollment when the username already exists.",
|
||||||
"Use the user's username, but deny enrollment when the username already"
|
|
||||||
" exists."
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
default="identifier",
|
default="identifier",
|
||||||
help_text=(
|
help_text="How the source determines if an existing user should be authenticated or a new user enrolled.",
|
||||||
"How the source determines if an existing user should be authenticated or a new"
|
|
||||||
" user enrolled."
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
@ -182,9 +167,7 @@ class Migration(migrations.Migration):
|
|||||||
model_name="application",
|
model_name="application",
|
||||||
name="meta_launch_url",
|
name="meta_launch_url",
|
||||||
field=models.TextField(
|
field=models.TextField(
|
||||||
blank=True,
|
blank=True, default="", validators=[authentik.lib.models.DomainlessURLValidator()]
|
||||||
default="",
|
|
||||||
validators=[authentik.lib.models.DomainlessFormattedURLValidator()],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
migrations.RunPython(
|
migrations.RunPython(
|
||||||
|
@ -4,6 +4,7 @@ from django.db import migrations, models
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("authentik_core", "0018_auto_20210330_1345_squashed_0028_alter_token_intent"),
|
("authentik_core", "0018_auto_20210330_1345_squashed_0028_alter_token_intent"),
|
||||||
]
|
]
|
||||||
|
@ -4,6 +4,7 @@ from django.db import migrations, models
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("authentik_core", "0019_application_group"),
|
("authentik_core", "0019_application_group"),
|
||||||
]
|
]
|
||||||
|
@ -4,6 +4,7 @@ from django.db import migrations, models
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("authentik_core", "0020_application_open_in_new_tab"),
|
("authentik_core", "0020_application_open_in_new_tab"),
|
||||||
]
|
]
|
||||||
|
@ -5,6 +5,7 @@ from django.db import migrations, models
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("authentik_core", "0021_source_user_path_user_path"),
|
("authentik_core", "0021_source_user_path_user_path"),
|
||||||
]
|
]
|
||||||
|
@ -4,6 +4,7 @@ from django.db import migrations, models
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("authentik_core", "0022_alter_group_parent"),
|
("authentik_core", "0022_alter_group_parent"),
|
||||||
]
|
]
|
||||||
|
@ -4,6 +4,7 @@ from django.db import migrations, models
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("authentik_core", "0023_source_authentik_c_slug_ccb2e5_idx_and_more"),
|
("authentik_core", "0023_source_authentik_c_slug_ccb2e5_idx_and_more"),
|
||||||
]
|
]
|
||||||
|
@ -1,25 +0,0 @@
|
|||||||
# Generated by Django 4.1.7 on 2023-03-02 21:32
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
dependencies = [
|
|
||||||
("authentik_flows", "0025_alter_flowstagebinding_evaluate_on_plan_and_more"),
|
|
||||||
("authentik_core", "0024_source_icon"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="provider",
|
|
||||||
name="authorization_flow",
|
|
||||||
field=models.ForeignKey(
|
|
||||||
help_text="Flow used when authorizing this provider.",
|
|
||||||
null=True,
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
related_name="provider_authorization",
|
|
||||||
to="authentik_flows.flow",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,26 +0,0 @@
|
|||||||
# Generated by Django 4.1.7 on 2023-03-07 13:41
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
from authentik.lib.migrations import fallback_names
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
dependencies = [
|
|
||||||
("authentik_core", "0025_alter_provider_authorization_flow"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.RunPython(fallback_names("authentik_core", "propertymapping", "name")),
|
|
||||||
migrations.RunPython(fallback_names("authentik_core", "provider", "name")),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="propertymapping",
|
|
||||||
name="name",
|
|
||||||
field=models.TextField(unique=True),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="provider",
|
|
||||||
name="name",
|
|
||||||
field=models.TextField(unique=True),
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,7 +1,8 @@
|
|||||||
"""authentik core models"""
|
"""authentik core models"""
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from hashlib import sha256
|
from hashlib import md5, sha256
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
|
from urllib.parse import urlencode
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from deepmerge import always_merger
|
from deepmerge import always_merger
|
||||||
@ -12,7 +13,9 @@ from django.contrib.auth.models import UserManager as DjangoUserManager
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import Q, QuerySet, options
|
from django.db.models import Q, QuerySet, options
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
|
from django.templatetags.static import static
|
||||||
from django.utils.functional import SimpleLazyObject, cached_property
|
from django.utils.functional import SimpleLazyObject, cached_property
|
||||||
|
from django.utils.html import escape
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from guardian.mixins import GuardianUserMixin
|
from guardian.mixins import GuardianUserMixin
|
||||||
@ -22,15 +25,11 @@ from structlog.stdlib import get_logger
|
|||||||
|
|
||||||
from authentik.blueprints.models import ManagedModel
|
from authentik.blueprints.models import ManagedModel
|
||||||
from authentik.core.exceptions import PropertyMappingExpressionException
|
from authentik.core.exceptions import PropertyMappingExpressionException
|
||||||
|
from authentik.core.signals import password_changed
|
||||||
from authentik.core.types import UILoginButton, UserSettingSerializer
|
from authentik.core.types import UILoginButton, UserSettingSerializer
|
||||||
from authentik.lib.avatars import get_avatar
|
from authentik.lib.config import CONFIG, get_path_from_dict
|
||||||
from authentik.lib.config import CONFIG
|
|
||||||
from authentik.lib.generators import generate_id
|
from authentik.lib.generators import generate_id
|
||||||
from authentik.lib.models import (
|
from authentik.lib.models import CreatedUpdatedModel, DomainlessURLValidator, SerializerModel
|
||||||
CreatedUpdatedModel,
|
|
||||||
DomainlessFormattedURLValidator,
|
|
||||||
SerializerModel,
|
|
||||||
)
|
|
||||||
from authentik.lib.utils.http import get_client_ip
|
from authentik.lib.utils.http import get_client_ip
|
||||||
from authentik.policies.models import PolicyBindingModel
|
from authentik.policies.models import PolicyBindingModel
|
||||||
|
|
||||||
@ -50,6 +49,9 @@ USER_ATTRIBUTE_CAN_OVERRIDE_IP = "goauthentik.io/user/override-ips"
|
|||||||
USER_PATH_SYSTEM_PREFIX = "goauthentik.io"
|
USER_PATH_SYSTEM_PREFIX = "goauthentik.io"
|
||||||
USER_PATH_SERVICE_ACCOUNT = USER_PATH_SYSTEM_PREFIX + "/service-accounts"
|
USER_PATH_SERVICE_ACCOUNT = USER_PATH_SYSTEM_PREFIX + "/service-accounts"
|
||||||
|
|
||||||
|
GRAVATAR_URL = "https://secure.gravatar.com"
|
||||||
|
DEFAULT_AVATAR = static("dist/assets/images/user_default.png")
|
||||||
|
|
||||||
|
|
||||||
options.DEFAULT_NAMES = options.DEFAULT_NAMES + ("authentik_used_by_shadows",)
|
options.DEFAULT_NAMES = options.DEFAULT_NAMES + ("authentik_used_by_shadows",)
|
||||||
|
|
||||||
@ -127,6 +129,7 @@ class Group(SerializerModel):
|
|||||||
return f"Group {self.name}"
|
return f"Group {self.name}"
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
unique_together = (
|
unique_together = (
|
||||||
(
|
(
|
||||||
"name",
|
"name",
|
||||||
@ -192,8 +195,6 @@ class User(SerializerModel, GuardianUserMixin, AbstractUser):
|
|||||||
|
|
||||||
def set_password(self, raw_password, signal=True):
|
def set_password(self, raw_password, signal=True):
|
||||||
if self.pk and signal:
|
if self.pk and signal:
|
||||||
from authentik.core.signals import password_changed
|
|
||||||
|
|
||||||
password_changed.send(sender=self, user=self, password=raw_password)
|
password_changed.send(sender=self, user=self, password=raw_password)
|
||||||
self.password_change_date = now()
|
self.password_change_date = now()
|
||||||
return super().set_password(raw_password)
|
return super().set_password(raw_password)
|
||||||
@ -233,9 +234,28 @@ class User(SerializerModel, GuardianUserMixin, AbstractUser):
|
|||||||
@property
|
@property
|
||||||
def avatar(self) -> str:
|
def avatar(self) -> str:
|
||||||
"""Get avatar, depending on authentik.avatar setting"""
|
"""Get avatar, depending on authentik.avatar setting"""
|
||||||
return get_avatar(self)
|
mode: str = CONFIG.y("avatars", "none")
|
||||||
|
if mode == "none":
|
||||||
|
return DEFAULT_AVATAR
|
||||||
|
if mode.startswith("attributes."):
|
||||||
|
return get_path_from_dict(self.attributes, mode[11:], default=DEFAULT_AVATAR)
|
||||||
|
# gravatar uses md5 for their URLs, so md5 can't be avoided
|
||||||
|
mail_hash = md5(self.email.lower().encode("utf-8")).hexdigest() # nosec
|
||||||
|
if mode == "gravatar":
|
||||||
|
parameters = [
|
||||||
|
("s", "158"),
|
||||||
|
("r", "g"),
|
||||||
|
]
|
||||||
|
gravatar_url = f"{GRAVATAR_URL}/avatar/{mail_hash}?{urlencode(parameters, doseq=True)}"
|
||||||
|
return escape(gravatar_url)
|
||||||
|
return mode % {
|
||||||
|
"username": self.username,
|
||||||
|
"mail_hash": mail_hash,
|
||||||
|
"upn": self.attributes.get("upn", ""),
|
||||||
|
}
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
permissions = (
|
permissions = (
|
||||||
("reset_user_password", "Reset Password"),
|
("reset_user_password", "Reset Password"),
|
||||||
("impersonate", "Can impersonate other users"),
|
("impersonate", "Can impersonate other users"),
|
||||||
@ -247,12 +267,11 @@ class User(SerializerModel, GuardianUserMixin, AbstractUser):
|
|||||||
class Provider(SerializerModel):
|
class Provider(SerializerModel):
|
||||||
"""Application-independent Provider instance. For example SAML2 Remote, OAuth2 Application"""
|
"""Application-independent Provider instance. For example SAML2 Remote, OAuth2 Application"""
|
||||||
|
|
||||||
name = models.TextField(unique=True)
|
name = models.TextField()
|
||||||
|
|
||||||
authorization_flow = models.ForeignKey(
|
authorization_flow = models.ForeignKey(
|
||||||
"authentik_flows.Flow",
|
"authentik_flows.Flow",
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
null=True,
|
|
||||||
help_text=_("Flow used when authorizing this provider."),
|
help_text=_("Flow used when authorizing this provider."),
|
||||||
related_name="provider_authorization",
|
related_name="provider_authorization",
|
||||||
)
|
)
|
||||||
@ -295,7 +314,7 @@ class Application(SerializerModel, PolicyBindingModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
meta_launch_url = models.TextField(
|
meta_launch_url = models.TextField(
|
||||||
default="", blank=True, validators=[DomainlessFormattedURLValidator()]
|
default="", blank=True, validators=[DomainlessURLValidator()]
|
||||||
)
|
)
|
||||||
|
|
||||||
open_in_new_tab = models.BooleanField(
|
open_in_new_tab = models.BooleanField(
|
||||||
@ -363,6 +382,7 @@ class Application(SerializerModel, PolicyBindingModel):
|
|||||||
return str(self.name)
|
return str(self.name)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
verbose_name = _("Application")
|
verbose_name = _("Application")
|
||||||
verbose_name_plural = _("Applications")
|
verbose_name_plural = _("Applications")
|
||||||
|
|
||||||
@ -372,15 +392,19 @@ class SourceUserMatchingModes(models.TextChoices):
|
|||||||
|
|
||||||
IDENTIFIER = "identifier", _("Use the source-specific identifier")
|
IDENTIFIER = "identifier", _("Use the source-specific identifier")
|
||||||
EMAIL_LINK = "email_link", _(
|
EMAIL_LINK = "email_link", _(
|
||||||
"Link to a user with identical email address. Can have security implications "
|
(
|
||||||
"when a source doesn't validate email addresses."
|
"Link to a user with identical email address. Can have security implications "
|
||||||
|
"when a source doesn't validate email addresses."
|
||||||
|
)
|
||||||
)
|
)
|
||||||
EMAIL_DENY = "email_deny", _(
|
EMAIL_DENY = "email_deny", _(
|
||||||
"Use the user's email address, but deny enrollment when the email address already exists."
|
"Use the user's email address, but deny enrollment when the email address already exists."
|
||||||
)
|
)
|
||||||
USERNAME_LINK = "username_link", _(
|
USERNAME_LINK = "username_link", _(
|
||||||
"Link to a user with identical username. Can have security implications "
|
(
|
||||||
"when a username is used with another source."
|
"Link to a user with identical username. Can have security implications "
|
||||||
|
"when a username is used with another source."
|
||||||
|
)
|
||||||
)
|
)
|
||||||
USERNAME_DENY = "username_deny", _(
|
USERNAME_DENY = "username_deny", _(
|
||||||
"Use the user's username, but deny enrollment when the username already exists."
|
"Use the user's username, but deny enrollment when the username already exists."
|
||||||
@ -427,8 +451,10 @@ class Source(ManagedModel, SerializerModel, PolicyBindingModel):
|
|||||||
choices=SourceUserMatchingModes.choices,
|
choices=SourceUserMatchingModes.choices,
|
||||||
default=SourceUserMatchingModes.IDENTIFIER,
|
default=SourceUserMatchingModes.IDENTIFIER,
|
||||||
help_text=_(
|
help_text=_(
|
||||||
"How the source determines if an existing user should be authenticated or "
|
(
|
||||||
"a new user enrolled."
|
"How the source determines if an existing user should be authenticated or "
|
||||||
|
"a new user enrolled."
|
||||||
|
)
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -474,6 +500,7 @@ class Source(ManagedModel, SerializerModel, PolicyBindingModel):
|
|||||||
return str(self.name)
|
return str(self.name)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
indexes = [
|
indexes = [
|
||||||
models.Index(
|
models.Index(
|
||||||
fields=[
|
fields=[
|
||||||
@ -502,6 +529,7 @@ class UserSourceConnection(SerializerModel, CreatedUpdatedModel):
|
|||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
unique_together = (("user", "source"),)
|
unique_together = (("user", "source"),)
|
||||||
|
|
||||||
|
|
||||||
@ -534,6 +562,7 @@ class ExpiringModel(models.Model):
|
|||||||
return now() > self.expires
|
return now() > self.expires
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
abstract = True
|
abstract = True
|
||||||
|
|
||||||
|
|
||||||
@ -599,6 +628,7 @@ class Token(SerializerModel, ManagedModel, ExpiringModel):
|
|||||||
return description
|
return description
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
verbose_name = _("Token")
|
verbose_name = _("Token")
|
||||||
verbose_name_plural = _("Tokens")
|
verbose_name_plural = _("Tokens")
|
||||||
indexes = [
|
indexes = [
|
||||||
@ -612,7 +642,7 @@ class PropertyMapping(SerializerModel, ManagedModel):
|
|||||||
"""User-defined key -> x mapping which can be used by providers to expose extra data."""
|
"""User-defined key -> x mapping which can be used by providers to expose extra data."""
|
||||||
|
|
||||||
pm_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4)
|
pm_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4)
|
||||||
name = models.TextField(unique=True)
|
name = models.TextField()
|
||||||
expression = models.TextField()
|
expression = models.TextField()
|
||||||
|
|
||||||
objects = InheritanceManager()
|
objects = InheritanceManager()
|
||||||
@ -635,12 +665,13 @@ class PropertyMapping(SerializerModel, ManagedModel):
|
|||||||
try:
|
try:
|
||||||
return evaluator.evaluate(self.expression)
|
return evaluator.evaluate(self.expression)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
raise PropertyMappingExpressionException(exc) from exc
|
raise PropertyMappingExpressionException(str(exc)) from exc
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"Property Mapping {self.name}"
|
return f"Property Mapping {self.name}"
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
verbose_name = _("Property Mapping")
|
verbose_name = _("Property Mapping")
|
||||||
verbose_name_plural = _("Property Mappings")
|
verbose_name_plural = _("Property Mappings")
|
||||||
|
|
||||||
@ -677,5 +708,6 @@ class AuthenticatedSession(ExpiringModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
verbose_name = _("Authenticated Session")
|
verbose_name = _("Authenticated Session")
|
||||||
verbose_name_plural = _("Authenticated Sessions")
|
verbose_name_plural = _("Authenticated Sessions")
|
||||||
|
@ -10,25 +10,25 @@ from django.db.models.signals import post_save, pre_delete
|
|||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.http.request import HttpRequest
|
from django.http.request import HttpRequest
|
||||||
|
|
||||||
from authentik.core.models import Application, AuthenticatedSession
|
|
||||||
|
|
||||||
# Arguments: user: User, password: str
|
# Arguments: user: User, password: str
|
||||||
password_changed = Signal()
|
password_changed = Signal()
|
||||||
# Arguments: credentials: dict[str, any], request: HttpRequest, stage: Stage
|
# Arguments: credentials: dict[str, any], request: HttpRequest, stage: Stage
|
||||||
login_failed = Signal()
|
login_failed = Signal()
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from authentik.core.models import User
|
from authentik.core.models import AuthenticatedSession, User
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_save, sender=Application)
|
@receiver(post_save)
|
||||||
def post_save_application(sender: type[Model], instance, created: bool, **_):
|
def post_save_application(sender: type[Model], instance, created: bool, **_):
|
||||||
"""Clear user's application cache upon application creation"""
|
"""Clear user's application cache upon application creation"""
|
||||||
from authentik.core.api.applications import user_app_cache_key
|
from authentik.core.api.applications import user_app_cache_key
|
||||||
|
from authentik.core.models import Application
|
||||||
|
|
||||||
|
if sender != Application:
|
||||||
|
return
|
||||||
if not created: # pragma: no cover
|
if not created: # pragma: no cover
|
||||||
return
|
return
|
||||||
|
|
||||||
# Also delete user application cache
|
# Also delete user application cache
|
||||||
keys = cache.keys(user_app_cache_key("*"))
|
keys = cache.keys(user_app_cache_key("*"))
|
||||||
cache.delete_many(keys)
|
cache.delete_many(keys)
|
||||||
@ -37,6 +37,7 @@ def post_save_application(sender: type[Model], instance, created: bool, **_):
|
|||||||
@receiver(user_logged_in)
|
@receiver(user_logged_in)
|
||||||
def user_logged_in_session(sender, request: HttpRequest, user: "User", **_):
|
def user_logged_in_session(sender, request: HttpRequest, user: "User", **_):
|
||||||
"""Create an AuthenticatedSession from request"""
|
"""Create an AuthenticatedSession from request"""
|
||||||
|
from authentik.core.models import AuthenticatedSession
|
||||||
|
|
||||||
session = AuthenticatedSession.from_request(request, user)
|
session = AuthenticatedSession.from_request(request, user)
|
||||||
if session:
|
if session:
|
||||||
@ -46,11 +47,18 @@ def user_logged_in_session(sender, request: HttpRequest, user: "User", **_):
|
|||||||
@receiver(user_logged_out)
|
@receiver(user_logged_out)
|
||||||
def user_logged_out_session(sender, request: HttpRequest, user: "User", **_):
|
def user_logged_out_session(sender, request: HttpRequest, user: "User", **_):
|
||||||
"""Delete AuthenticatedSession if it exists"""
|
"""Delete AuthenticatedSession if it exists"""
|
||||||
|
from authentik.core.models import AuthenticatedSession
|
||||||
|
|
||||||
AuthenticatedSession.objects.filter(session_key=request.session.session_key).delete()
|
AuthenticatedSession.objects.filter(session_key=request.session.session_key).delete()
|
||||||
|
|
||||||
|
|
||||||
@receiver(pre_delete, sender=AuthenticatedSession)
|
@receiver(pre_delete)
|
||||||
def authenticated_session_delete(sender: type[Model], instance: "AuthenticatedSession", **_):
|
def authenticated_session_delete(sender: type[Model], instance: "AuthenticatedSession", **_):
|
||||||
"""Delete session when authenticated session is deleted"""
|
"""Delete session when authenticated session is deleted"""
|
||||||
|
from authentik.core.models import AuthenticatedSession
|
||||||
|
|
||||||
|
if sender != AuthenticatedSession:
|
||||||
|
return
|
||||||
|
|
||||||
cache_key = f"{KEY_PREFIX}{instance.session_key}"
|
cache_key = f"{KEY_PREFIX}{instance.session_key}"
|
||||||
cache.delete(cache_key)
|
cache.delete(cache_key)
|
||||||
|
@ -190,8 +190,11 @@ class SourceFlowManager:
|
|||||||
# Default case, assume deny
|
# Default case, assume deny
|
||||||
error = Exception(
|
error = Exception(
|
||||||
_(
|
_(
|
||||||
"Request to authenticate with %(source)s has been denied. Please authenticate "
|
(
|
||||||
"with the source you've previously signed up with." % {"source": self.source.name}
|
"Request to authenticate with %(source)s has been denied. Please authenticate "
|
||||||
|
"with the source you've previously signed up with."
|
||||||
|
)
|
||||||
|
% {"source": self.source.name}
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
return self.error_handler(error)
|
return self.error_handler(error)
|
||||||
|
@ -43,12 +43,7 @@ def clean_expired_models(self: MonitoredTask):
|
|||||||
amount = 0
|
amount = 0
|
||||||
for session in AuthenticatedSession.objects.all():
|
for session in AuthenticatedSession.objects.all():
|
||||||
cache_key = f"{KEY_PREFIX}{session.session_key}"
|
cache_key = f"{KEY_PREFIX}{session.session_key}"
|
||||||
value = None
|
value = cache.get(cache_key)
|
||||||
try:
|
|
||||||
value = cache.get(cache_key)
|
|
||||||
# pylint: disable=broad-except
|
|
||||||
except Exception as exc:
|
|
||||||
LOGGER.debug("Failed to get session from cache", exc=exc)
|
|
||||||
if not value:
|
if not value:
|
||||||
session.delete()
|
session.delete()
|
||||||
amount += 1
|
amount += 1
|
||||||
|
@ -13,11 +13,11 @@
|
|||||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/page.css' %}">
|
<link rel="stylesheet" type="text/css" href="{% static 'dist/page.css' %}">
|
||||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/empty-state.css' %}">
|
<link rel="stylesheet" type="text/css" href="{% static 'dist/empty-state.css' %}">
|
||||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/spinner.css' %}">
|
<link rel="stylesheet" type="text/css" href="{% static 'dist/spinner.css' %}">
|
||||||
|
<link rel="stylesheet" type="text/css" href="{% static 'dist/dropdown.css' %}">
|
||||||
{% block head_before %}
|
{% block head_before %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}">
|
<link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}">
|
||||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/theme-dark.css' %}" media="(prefers-color-scheme: dark)">
|
<link rel="stylesheet" type="text/css" href="{% static 'dist/custom.css' %}">
|
||||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/custom.css' %}" data-inject>
|
|
||||||
<script src="{% static 'dist/poly.js' %}" type="module"></script>
|
<script src="{% static 'dist/poly.js' %}" type="module"></script>
|
||||||
{% block head %}
|
{% block head %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -21,15 +21,9 @@ You've logged out of {{ application }}.
|
|||||||
{% endblocktrans %}
|
{% endblocktrans %}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<a id="ak-back-home" href="{% url 'authentik_core:root-redirect' %}" class="pf-c-button pf-m-primary">
|
<a id="ak-back-home" href="{% url 'authentik_core:root-redirect' %}" class="pf-c-button pf-m-primary">{% trans 'Go back to overview' %}</a>
|
||||||
{% trans 'Go back to overview' %}
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a id="logout" href="{% url 'authentik_flows:default-invalidation' %}" class="pf-c-button pf-m-secondary">
|
<a id="logout" href="{% url 'authentik_flows:default-invalidation' %}" class="pf-c-button pf-m-secondary">{% trans 'Log out of authentik' %}</a>
|
||||||
{% blocktrans with branding_title=tenant.branding_title %}
|
|
||||||
Log out of {{ branding_title }}
|
|
||||||
{% endblocktrans %}
|
|
||||||
</a>
|
|
||||||
|
|
||||||
{% if application.get_launch_url %}
|
{% if application.get_launch_url %}
|
||||||
<a href="{{ application.get_launch_url }}" class="pf-c-button pf-m-secondary">
|
<a href="{{ application.get_launch_url }}" class="pf-c-button pf-m-secondary">
|
||||||
|
@ -37,22 +37,6 @@ class TestApplicationsAPI(APITestCase):
|
|||||||
order=0,
|
order=0,
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_formatted_launch_url(self):
|
|
||||||
"""Test formatted launch URL"""
|
|
||||||
self.client.force_login(self.user)
|
|
||||||
self.assertEqual(
|
|
||||||
self.client.patch(
|
|
||||||
reverse("authentik_api:application-detail", kwargs={"slug": self.allowed.slug}),
|
|
||||||
{"meta_launch_url": "https://%(username)s.test.goauthentik.io/%(username)s"},
|
|
||||||
).status_code,
|
|
||||||
200,
|
|
||||||
)
|
|
||||||
self.allowed.refresh_from_db()
|
|
||||||
self.assertEqual(
|
|
||||||
self.allowed.get_launch_url(self.user),
|
|
||||||
f"https://{self.user.username}.test.goauthentik.io/{self.user.username}",
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_set_icon(self):
|
def test_set_icon(self):
|
||||||
"""Test set_icon"""
|
"""Test set_icon"""
|
||||||
file = ContentFile(b"text", "name")
|
file = ContentFile(b"text", "name")
|
||||||
|
@ -5,10 +5,8 @@ from django.urls.base import reverse
|
|||||||
from guardian.shortcuts import get_anonymous_user
|
from guardian.shortcuts import get_anonymous_user
|
||||||
from rest_framework.test import APITestCase
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
from authentik.core.api.tokens import TokenSerializer
|
|
||||||
from authentik.core.models import USER_ATTRIBUTE_TOKEN_EXPIRING, Token, TokenIntents, User
|
from authentik.core.models import USER_ATTRIBUTE_TOKEN_EXPIRING, Token, TokenIntents, User
|
||||||
from authentik.core.tests.utils import create_test_admin_user
|
from authentik.core.tests.utils import create_test_admin_user
|
||||||
from authentik.lib.generators import generate_id
|
|
||||||
|
|
||||||
|
|
||||||
class TestTokenAPI(APITestCase):
|
class TestTokenAPI(APITestCase):
|
||||||
@ -32,28 +30,6 @@ class TestTokenAPI(APITestCase):
|
|||||||
self.assertEqual(token.expiring, True)
|
self.assertEqual(token.expiring, True)
|
||||||
self.assertTrue(self.user.has_perm("authentik_core.view_token_key", token))
|
self.assertTrue(self.user.has_perm("authentik_core.view_token_key", token))
|
||||||
|
|
||||||
def test_token_set_key(self):
|
|
||||||
"""Test token creation endpoint"""
|
|
||||||
response = self.client.post(
|
|
||||||
reverse("authentik_api:token-list"), {"identifier": "test-token"}
|
|
||||||
)
|
|
||||||
self.assertEqual(response.status_code, 201)
|
|
||||||
token = Token.objects.get(identifier="test-token")
|
|
||||||
self.assertEqual(token.user, self.user)
|
|
||||||
self.assertEqual(token.intent, TokenIntents.INTENT_API)
|
|
||||||
self.assertEqual(token.expiring, True)
|
|
||||||
self.assertTrue(self.user.has_perm("authentik_core.view_token_key", token))
|
|
||||||
|
|
||||||
self.client.force_login(self.admin)
|
|
||||||
new_key = generate_id()
|
|
||||||
response = self.client.post(
|
|
||||||
reverse("authentik_api:token-set-key", kwargs={"identifier": token.identifier}),
|
|
||||||
{"key": new_key},
|
|
||||||
)
|
|
||||||
self.assertEqual(response.status_code, 204)
|
|
||||||
token.refresh_from_db()
|
|
||||||
self.assertEqual(token.key, new_key)
|
|
||||||
|
|
||||||
def test_token_create_invalid(self):
|
def test_token_create_invalid(self):
|
||||||
"""Test token creation endpoint (invalid data)"""
|
"""Test token creation endpoint (invalid data)"""
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
@ -81,7 +57,7 @@ class TestTokenAPI(APITestCase):
|
|||||||
identifier="test", expiring=False, user=self.user
|
identifier="test", expiring=False, user=self.user
|
||||||
)
|
)
|
||||||
Token.objects.create(identifier="test-2", expiring=False, user=get_anonymous_user())
|
Token.objects.create(identifier="test-2", expiring=False, user=get_anonymous_user())
|
||||||
response = self.client.get(reverse("authentik_api:token-list"))
|
response = self.client.get(reverse(("authentik_api:token-list")))
|
||||||
body = loads(response.content)
|
body = loads(response.content)
|
||||||
self.assertEqual(len(body["results"]), 1)
|
self.assertEqual(len(body["results"]), 1)
|
||||||
self.assertEqual(body["results"][0]["identifier"], token_should.identifier)
|
self.assertEqual(body["results"][0]["identifier"], token_should.identifier)
|
||||||
@ -95,21 +71,8 @@ class TestTokenAPI(APITestCase):
|
|||||||
token_should_not: Token = Token.objects.create(
|
token_should_not: Token = Token.objects.create(
|
||||||
identifier="test-2", expiring=False, user=get_anonymous_user()
|
identifier="test-2", expiring=False, user=get_anonymous_user()
|
||||||
)
|
)
|
||||||
response = self.client.get(reverse("authentik_api:token-list"))
|
response = self.client.get(reverse(("authentik_api:token-list")))
|
||||||
body = loads(response.content)
|
body = loads(response.content)
|
||||||
self.assertEqual(len(body["results"]), 2)
|
self.assertEqual(len(body["results"]), 2)
|
||||||
self.assertEqual(body["results"][0]["identifier"], token_should.identifier)
|
self.assertEqual(body["results"][0]["identifier"], token_should.identifier)
|
||||||
self.assertEqual(body["results"][1]["identifier"], token_should_not.identifier)
|
self.assertEqual(body["results"][1]["identifier"], token_should_not.identifier)
|
||||||
|
|
||||||
def test_serializer_no_request(self):
|
|
||||||
"""Test serializer without request"""
|
|
||||||
self.assertTrue(
|
|
||||||
TokenSerializer(
|
|
||||||
data={
|
|
||||||
"identifier": generate_id(),
|
|
||||||
"intent": TokenIntents.INTENT_APP_PASSWORD,
|
|
||||||
"key": generate_id(),
|
|
||||||
"user": self.user.pk,
|
|
||||||
}
|
|
||||||
).is_valid(raise_exception=True)
|
|
||||||
)
|
|
||||||
|
@ -1,21 +1,15 @@
|
|||||||
"""Test Users API"""
|
"""Test Users API"""
|
||||||
|
from json import loads
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
from django.contrib.sessions.backends.cache import KEY_PREFIX
|
from django.contrib.sessions.backends.cache import KEY_PREFIX
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.urls.base import reverse
|
from django.urls.base import reverse
|
||||||
from rest_framework.test import APITestCase
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
from authentik.core.models import (
|
from authentik.core.models import AuthenticatedSession, User
|
||||||
USER_ATTRIBUTE_SA,
|
|
||||||
USER_ATTRIBUTE_TOKEN_EXPIRING,
|
|
||||||
AuthenticatedSession,
|
|
||||||
Token,
|
|
||||||
User,
|
|
||||||
)
|
|
||||||
from authentik.core.tests.utils import create_test_admin_user, create_test_flow, create_test_tenant
|
from authentik.core.tests.utils import create_test_admin_user, create_test_flow, create_test_tenant
|
||||||
from authentik.flows.models import FlowDesignation
|
from authentik.flows.models import FlowDesignation
|
||||||
|
from authentik.lib.config import CONFIG
|
||||||
from authentik.lib.generators import generate_id, generate_key
|
from authentik.lib.generators import generate_id, generate_key
|
||||||
from authentik.stages.email.models import EmailStage
|
from authentik.stages.email.models import EmailStage
|
||||||
from authentik.tenants.models import Tenant
|
from authentik.tenants.models import Tenant
|
||||||
@ -138,71 +132,7 @@ class TestUsersAPI(APITestCase):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertTrue(User.objects.filter(username="test-sa").exists())
|
||||||
user_filter = User.objects.filter(
|
|
||||||
username="test-sa",
|
|
||||||
attributes={USER_ATTRIBUTE_TOKEN_EXPIRING: True, USER_ATTRIBUTE_SA: True},
|
|
||||||
)
|
|
||||||
self.assertTrue(user_filter.exists())
|
|
||||||
user: User = user_filter.first()
|
|
||||||
self.assertFalse(user.has_usable_password())
|
|
||||||
|
|
||||||
token_filter = Token.objects.filter(user=user)
|
|
||||||
self.assertTrue(token_filter.exists())
|
|
||||||
self.assertTrue(token_filter.first().expiring)
|
|
||||||
|
|
||||||
def test_service_account_no_expire(self):
|
|
||||||
"""Service account creation without token expiration"""
|
|
||||||
self.client.force_login(self.admin)
|
|
||||||
response = self.client.post(
|
|
||||||
reverse("authentik_api:user-service-account"),
|
|
||||||
data={
|
|
||||||
"name": "test-sa",
|
|
||||||
"create_group": True,
|
|
||||||
"expiring": False,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
|
|
||||||
user_filter = User.objects.filter(
|
|
||||||
username="test-sa",
|
|
||||||
attributes={USER_ATTRIBUTE_TOKEN_EXPIRING: False, USER_ATTRIBUTE_SA: True},
|
|
||||||
)
|
|
||||||
self.assertTrue(user_filter.exists())
|
|
||||||
user: User = user_filter.first()
|
|
||||||
self.assertFalse(user.has_usable_password())
|
|
||||||
|
|
||||||
token_filter = Token.objects.filter(user=user)
|
|
||||||
self.assertTrue(token_filter.exists())
|
|
||||||
self.assertFalse(token_filter.first().expiring)
|
|
||||||
|
|
||||||
def test_service_account_with_custom_expire(self):
|
|
||||||
"""Service account creation with custom token expiration date"""
|
|
||||||
self.client.force_login(self.admin)
|
|
||||||
expire_on = datetime(2050, 11, 11, 11, 11, 11).astimezone()
|
|
||||||
response = self.client.post(
|
|
||||||
reverse("authentik_api:user-service-account"),
|
|
||||||
data={
|
|
||||||
"name": "test-sa",
|
|
||||||
"create_group": True,
|
|
||||||
"expires": expire_on.isoformat(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
|
|
||||||
user_filter = User.objects.filter(
|
|
||||||
username="test-sa",
|
|
||||||
attributes={USER_ATTRIBUTE_TOKEN_EXPIRING: True, USER_ATTRIBUTE_SA: True},
|
|
||||||
)
|
|
||||||
self.assertTrue(user_filter.exists())
|
|
||||||
user: User = user_filter.first()
|
|
||||||
self.assertFalse(user.has_usable_password())
|
|
||||||
|
|
||||||
token_filter = Token.objects.filter(user=user)
|
|
||||||
self.assertTrue(token_filter.exists())
|
|
||||||
token = token_filter.first()
|
|
||||||
self.assertTrue(token.expiring)
|
|
||||||
self.assertEqual(token.expires, expire_on)
|
|
||||||
|
|
||||||
def test_service_account_invalid(self):
|
def test_service_account_invalid(self):
|
||||||
"""Service account creation (twice with same name, expect error)"""
|
"""Service account creation (twice with same name, expect error)"""
|
||||||
@ -215,19 +145,7 @@ class TestUsersAPI(APITestCase):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertTrue(User.objects.filter(username="test-sa").exists())
|
||||||
user_filter = User.objects.filter(
|
|
||||||
username="test-sa",
|
|
||||||
attributes={USER_ATTRIBUTE_TOKEN_EXPIRING: True, USER_ATTRIBUTE_SA: True},
|
|
||||||
)
|
|
||||||
self.assertTrue(user_filter.exists())
|
|
||||||
user: User = user_filter.first()
|
|
||||||
self.assertFalse(user.has_usable_password())
|
|
||||||
|
|
||||||
token_filter = Token.objects.filter(user=user)
|
|
||||||
self.assertTrue(token_filter.exists())
|
|
||||||
self.assertTrue(token_filter.first().expiring)
|
|
||||||
|
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse("authentik_api:user-service-account"),
|
reverse("authentik_api:user-service-account"),
|
||||||
data={
|
data={
|
||||||
@ -304,6 +222,44 @@ class TestUsersAPI(APITestCase):
|
|||||||
response = self.client.get(reverse("authentik_api:user-me"))
|
response = self.client.get(reverse("authentik_api:user-me"))
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
@CONFIG.patch("avatars", "none")
|
||||||
|
def test_avatars_none(self):
|
||||||
|
"""Test avatars none"""
|
||||||
|
self.client.force_login(self.admin)
|
||||||
|
response = self.client.get(reverse("authentik_api:user-me"))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
body = loads(response.content.decode())
|
||||||
|
self.assertEqual(body["user"]["avatar"], "/static/dist/assets/images/user_default.png")
|
||||||
|
|
||||||
|
@CONFIG.patch("avatars", "gravatar")
|
||||||
|
def test_avatars_gravatar(self):
|
||||||
|
"""Test avatars gravatar"""
|
||||||
|
self.client.force_login(self.admin)
|
||||||
|
response = self.client.get(reverse("authentik_api:user-me"))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
body = loads(response.content.decode())
|
||||||
|
self.assertIn("gravatar", body["user"]["avatar"])
|
||||||
|
|
||||||
|
@CONFIG.patch("avatars", "foo-%(username)s")
|
||||||
|
def test_avatars_custom(self):
|
||||||
|
"""Test avatars custom"""
|
||||||
|
self.client.force_login(self.admin)
|
||||||
|
response = self.client.get(reverse("authentik_api:user-me"))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
body = loads(response.content.decode())
|
||||||
|
self.assertEqual(body["user"]["avatar"], f"foo-{self.admin.username}")
|
||||||
|
|
||||||
|
@CONFIG.patch("avatars", "attributes.foo.avatar")
|
||||||
|
def test_avatars_attributes(self):
|
||||||
|
"""Test avatars attributes"""
|
||||||
|
self.admin.attributes = {"foo": {"avatar": "bar"}}
|
||||||
|
self.admin.save()
|
||||||
|
self.client.force_login(self.admin)
|
||||||
|
response = self.client.get(reverse("authentik_api:user-me"))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
body = loads(response.content.decode())
|
||||||
|
self.assertEqual(body["user"]["avatar"], "bar")
|
||||||
|
|
||||||
def test_session_delete(self):
|
def test_session_delete(self):
|
||||||
"""Ensure sessions are deleted when a user is deactivated"""
|
"""Ensure sessions are deleted when a user is deactivated"""
|
||||||
user = create_test_admin_user()
|
user = create_test_admin_user()
|
||||||
|
@ -1,84 +0,0 @@
|
|||||||
"""Test Users Avatars"""
|
|
||||||
from json import loads
|
|
||||||
|
|
||||||
from django.urls.base import reverse
|
|
||||||
from requests_mock import Mocker
|
|
||||||
from rest_framework.test import APITestCase
|
|
||||||
|
|
||||||
from authentik.core.models import User
|
|
||||||
from authentik.core.tests.utils import create_test_admin_user
|
|
||||||
from authentik.lib.config import CONFIG
|
|
||||||
|
|
||||||
|
|
||||||
class TestUsersAvatars(APITestCase):
|
|
||||||
"""Test Users avatars"""
|
|
||||||
|
|
||||||
def setUp(self) -> None:
|
|
||||||
self.admin = create_test_admin_user()
|
|
||||||
self.user = User.objects.create(username="test-user")
|
|
||||||
|
|
||||||
@CONFIG.patch("avatars", "none")
|
|
||||||
def test_avatars_none(self):
|
|
||||||
"""Test avatars none"""
|
|
||||||
self.client.force_login(self.admin)
|
|
||||||
response = self.client.get(reverse("authentik_api:user-me"))
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
body = loads(response.content.decode())
|
|
||||||
self.assertEqual(body["user"]["avatar"], "/static/dist/assets/images/user_default.png")
|
|
||||||
|
|
||||||
@CONFIG.patch("avatars", "gravatar")
|
|
||||||
def test_avatars_gravatar(self):
|
|
||||||
"""Test avatars gravatar"""
|
|
||||||
self.admin.email = "static@t.goauthentik.io"
|
|
||||||
self.admin.save()
|
|
||||||
self.client.force_login(self.admin)
|
|
||||||
with Mocker() as mocker:
|
|
||||||
mocker.head(
|
|
||||||
(
|
|
||||||
"https://secure.gravatar.com/avatar/84730f9c1851d1ea03f1a"
|
|
||||||
"a9ed85bd1ea?size=158&rating=g&default=404"
|
|
||||||
),
|
|
||||||
text="foo",
|
|
||||||
)
|
|
||||||
response = self.client.get(reverse("authentik_api:user-me"))
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
body = loads(response.content.decode())
|
|
||||||
self.assertIn("gravatar", body["user"]["avatar"])
|
|
||||||
|
|
||||||
@CONFIG.patch("avatars", "initials")
|
|
||||||
def test_avatars_initials(self):
|
|
||||||
"""Test avatars initials"""
|
|
||||||
self.client.force_login(self.admin)
|
|
||||||
response = self.client.get(reverse("authentik_api:user-me"))
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
body = loads(response.content.decode())
|
|
||||||
self.assertIn("data:image/svg+xml;base64,", body["user"]["avatar"])
|
|
||||||
|
|
||||||
@CONFIG.patch("avatars", "foo://%(username)s")
|
|
||||||
def test_avatars_custom(self):
|
|
||||||
"""Test avatars custom"""
|
|
||||||
self.client.force_login(self.admin)
|
|
||||||
response = self.client.get(reverse("authentik_api:user-me"))
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
body = loads(response.content.decode())
|
|
||||||
self.assertEqual(body["user"]["avatar"], f"foo://{self.admin.username}")
|
|
||||||
|
|
||||||
@CONFIG.patch("avatars", "attributes.foo.avatar")
|
|
||||||
def test_avatars_attributes(self):
|
|
||||||
"""Test avatars attributes"""
|
|
||||||
self.admin.attributes = {"foo": {"avatar": "bar"}}
|
|
||||||
self.admin.save()
|
|
||||||
self.client.force_login(self.admin)
|
|
||||||
response = self.client.get(reverse("authentik_api:user-me"))
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
body = loads(response.content.decode())
|
|
||||||
self.assertEqual(body["user"]["avatar"], "bar")
|
|
||||||
|
|
||||||
@CONFIG.patch("avatars", "attributes.foo.avatar,initials")
|
|
||||||
def test_avatars_fallback(self):
|
|
||||||
"""Test fallback"""
|
|
||||||
self.client.force_login(self.admin)
|
|
||||||
response = self.client.get(reverse("authentik_api:user-me"))
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
body = loads(response.content.decode())
|
|
||||||
self.assertIn("data:image/svg+xml;base64,", body["user"]["avatar"])
|
|
@ -11,7 +11,6 @@ from authentik.flows.challenge import (
|
|||||||
HttpChallengeResponse,
|
HttpChallengeResponse,
|
||||||
RedirectChallenge,
|
RedirectChallenge,
|
||||||
)
|
)
|
||||||
from authentik.flows.exceptions import FlowNonApplicableException
|
|
||||||
from authentik.flows.models import in_memory_stage
|
from authentik.flows.models import in_memory_stage
|
||||||
from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, FlowPlanner
|
from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, FlowPlanner
|
||||||
from authentik.flows.stage import ChallengeStageView
|
from authentik.flows.stage import ChallengeStageView
|
||||||
@ -42,18 +41,15 @@ class RedirectToAppLaunch(View):
|
|||||||
flow = tenant.flow_authentication
|
flow = tenant.flow_authentication
|
||||||
planner = FlowPlanner(flow)
|
planner = FlowPlanner(flow)
|
||||||
planner.allow_empty_flows = True
|
planner.allow_empty_flows = True
|
||||||
try:
|
plan = planner.plan(
|
||||||
plan = planner.plan(
|
request,
|
||||||
request,
|
{
|
||||||
{
|
PLAN_CONTEXT_APPLICATION: app,
|
||||||
PLAN_CONTEXT_APPLICATION: app,
|
PLAN_CONTEXT_CONSENT_HEADER: _("You're about to sign into %(application)s.")
|
||||||
PLAN_CONTEXT_CONSENT_HEADER: _("You're about to sign into %(application)s.")
|
% {"application": app.name},
|
||||||
% {"application": app.name},
|
PLAN_CONTEXT_CONSENT_PERMISSIONS: [],
|
||||||
PLAN_CONTEXT_CONSENT_PERMISSIONS: [],
|
},
|
||||||
},
|
)
|
||||||
)
|
|
||||||
except FlowNonApplicableException:
|
|
||||||
raise Http404
|
|
||||||
plan.insert_stage(in_memory_stage(RedirectToAppStage))
|
plan.insert_stage(in_memory_stage(RedirectToAppStage))
|
||||||
request.session[SESSION_KEY_PLAN] = plan
|
request.session[SESSION_KEY_PLAN] = plan
|
||||||
return redirect_with_qs("authentik_core:if-flow", request.GET, flow_slug=flow.slug)
|
return redirect_with_qs("authentik_core:if-flow", request.GET, flow_slug=flow.slug)
|
||||||
|
@ -143,6 +143,7 @@ class CertificateKeyPairSerializer(ModelSerializer):
|
|||||||
return value
|
return value
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
model = CertificateKeyPair
|
model = CertificateKeyPair
|
||||||
fields = [
|
fields = [
|
||||||
"pk",
|
"pk",
|
||||||
|
@ -6,6 +6,7 @@ from django.db import migrations, models
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
initial = True
|
initial = True
|
||||||
|
|
||||||
dependencies = []
|
dependencies = []
|
||||||
@ -35,10 +36,7 @@ class Migration(migrations.Migration):
|
|||||||
models.TextField(
|
models.TextField(
|
||||||
blank=True,
|
blank=True,
|
||||||
default="",
|
default="",
|
||||||
help_text=(
|
help_text="Optional Private Key. If this is set, you can use this keypair for encryption.",
|
||||||
"Optional Private Key. If this is set, you can use this keypair for"
|
|
||||||
" encryption."
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -6,6 +6,7 @@ from authentik.lib.generators import generate_id
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("authentik_crypto", "0001_initial"),
|
("authentik_crypto", "0001_initial"),
|
||||||
]
|
]
|
||||||
|
@ -4,6 +4,7 @@ from django.db import migrations, models
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("authentik_crypto", "0002_create_self_signed_kp"),
|
("authentik_crypto", "0002_create_self_signed_kp"),
|
||||||
]
|
]
|
||||||
@ -14,12 +15,7 @@ class Migration(migrations.Migration):
|
|||||||
name="managed",
|
name="managed",
|
||||||
field=models.TextField(
|
field=models.TextField(
|
||||||
default=None,
|
default=None,
|
||||||
help_text=(
|
help_text="Objects which are managed by authentik. These objects are created and updated automatically. This is flag only indicates that an object can be overwritten by migrations. You can still modify the objects via the API, but expect changes to be overwritten in a later update.",
|
||||||
"Objects which are managed by authentik. These objects are created and updated"
|
|
||||||
" automatically. This is flag only indicates that an object can be overwritten"
|
|
||||||
" by migrations. You can still modify the objects via the API, but expect"
|
|
||||||
" changes to be overwritten in a later update."
|
|
||||||
),
|
|
||||||
null=True,
|
null=True,
|
||||||
unique=True,
|
unique=True,
|
||||||
verbose_name="Managed by authentik",
|
verbose_name="Managed by authentik",
|
||||||
|
@ -98,5 +98,6 @@ class CertificateKeyPair(SerializerModel, ManagedModel, CreatedUpdatedModel):
|
|||||||
return f"Certificate-Key Pair {self.name}"
|
return f"Certificate-Key Pair {self.name}"
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
verbose_name = _("Certificate-Key Pair")
|
verbose_name = _("Certificate-Key Pair")
|
||||||
verbose_name_plural = _("Certificate-Key Pairs")
|
verbose_name_plural = _("Certificate-Key Pairs")
|
||||||
|
@ -25,6 +25,7 @@ class EventSerializer(ModelSerializer):
|
|||||||
"""Event Serializer"""
|
"""Event Serializer"""
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
model = Event
|
model = Event
|
||||||
fields = [
|
fields = [
|
||||||
"pk",
|
"pk",
|
||||||
|
@ -10,6 +10,7 @@ class NotificationWebhookMappingSerializer(ModelSerializer):
|
|||||||
"""NotificationWebhookMapping Serializer"""
|
"""NotificationWebhookMapping Serializer"""
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
model = NotificationWebhookMapping
|
model = NotificationWebhookMapping
|
||||||
fields = [
|
fields = [
|
||||||
"pk",
|
"pk",
|
||||||
|
@ -13,6 +13,7 @@ class NotificationRuleSerializer(ModelSerializer):
|
|||||||
group_obj = GroupSerializer(read_only=True, source="group")
|
group_obj = GroupSerializer(read_only=True, source="group")
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
model = NotificationRule
|
model = NotificationRule
|
||||||
fields = [
|
fields = [
|
||||||
"pk",
|
"pk",
|
||||||
|
@ -43,6 +43,7 @@ class NotificationTransportSerializer(ModelSerializer):
|
|||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
model = NotificationTransport
|
model = NotificationTransport
|
||||||
fields = [
|
fields = [
|
||||||
"pk",
|
"pk",
|
||||||
|
@ -25,6 +25,7 @@ class NotificationSerializer(ModelSerializer):
|
|||||||
event = EventSerializer(required=False)
|
event = EventSerializer(required=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
model = Notification
|
model = Notification
|
||||||
fields = [
|
fields = [
|
||||||
"pk",
|
"pk",
|
||||||
|
@ -7,14 +7,13 @@ from django.conf import settings
|
|||||||
from django.contrib.sessions.models import Session
|
from django.contrib.sessions.models import Session
|
||||||
from django.core.exceptions import SuspiciousOperation
|
from django.core.exceptions import SuspiciousOperation
|
||||||
from django.db.models import Model
|
from django.db.models import Model
|
||||||
from django.db.models.signals import m2m_changed, post_save, pre_delete
|
from django.db.models.signals import post_save, pre_delete
|
||||||
from django.http import HttpRequest, HttpResponse
|
from django.http import HttpRequest, HttpResponse
|
||||||
from django_otp.plugins.otp_static.models import StaticToken
|
from django_otp.plugins.otp_static.models import StaticToken
|
||||||
from guardian.models import UserObjectPermission
|
from guardian.models import UserObjectPermission
|
||||||
|
|
||||||
from authentik.core.models import (
|
from authentik.core.models import (
|
||||||
AuthenticatedSession,
|
AuthenticatedSession,
|
||||||
Group,
|
|
||||||
PropertyMapping,
|
PropertyMapping,
|
||||||
Provider,
|
Provider,
|
||||||
Source,
|
Source,
|
||||||
@ -28,8 +27,6 @@ from authentik.lib.sentry import before_send
|
|||||||
from authentik.lib.utils.errors import exception_to_string
|
from authentik.lib.utils.errors import exception_to_string
|
||||||
from authentik.outposts.models import OutpostServiceConnection
|
from authentik.outposts.models import OutpostServiceConnection
|
||||||
from authentik.policies.models import Policy, PolicyBindingModel
|
from authentik.policies.models import Policy, PolicyBindingModel
|
||||||
from authentik.providers.oauth2.models import AccessToken, AuthorizationCode, RefreshToken
|
|
||||||
from authentik.providers.scim.models import SCIMGroup, SCIMUser
|
|
||||||
|
|
||||||
IGNORED_MODELS = (
|
IGNORED_MODELS = (
|
||||||
Event,
|
Event,
|
||||||
@ -47,11 +44,6 @@ IGNORED_MODELS = (
|
|||||||
OutpostServiceConnection,
|
OutpostServiceConnection,
|
||||||
Policy,
|
Policy,
|
||||||
PolicyBindingModel,
|
PolicyBindingModel,
|
||||||
AuthorizationCode,
|
|
||||||
AccessToken,
|
|
||||||
RefreshToken,
|
|
||||||
SCIMUser,
|
|
||||||
SCIMGroup,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -62,13 +54,6 @@ def should_log_model(model: Model) -> bool:
|
|||||||
return model.__class__ not in IGNORED_MODELS
|
return model.__class__ not in IGNORED_MODELS
|
||||||
|
|
||||||
|
|
||||||
def should_log_m2m(model: Model) -> bool:
|
|
||||||
"""Return true if m2m operation should be logged"""
|
|
||||||
if model.__class__ in [User, Group]:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
class EventNewThread(Thread):
|
class EventNewThread(Thread):
|
||||||
"""Create Event in background thread"""
|
"""Create Event in background thread"""
|
||||||
|
|
||||||
@ -107,7 +92,6 @@ class AuditMiddleware:
|
|||||||
return
|
return
|
||||||
post_save_handler = partial(self.post_save_handler, user=request.user, request=request)
|
post_save_handler = partial(self.post_save_handler, user=request.user, request=request)
|
||||||
pre_delete_handler = partial(self.pre_delete_handler, user=request.user, request=request)
|
pre_delete_handler = partial(self.pre_delete_handler, user=request.user, request=request)
|
||||||
m2m_changed_handler = partial(self.m2m_changed_handler, user=request.user, request=request)
|
|
||||||
post_save.connect(
|
post_save.connect(
|
||||||
post_save_handler,
|
post_save_handler,
|
||||||
dispatch_uid=request.request_id,
|
dispatch_uid=request.request_id,
|
||||||
@ -118,11 +102,6 @@ class AuditMiddleware:
|
|||||||
dispatch_uid=request.request_id,
|
dispatch_uid=request.request_id,
|
||||||
weak=False,
|
weak=False,
|
||||||
)
|
)
|
||||||
m2m_changed.connect(
|
|
||||||
m2m_changed_handler,
|
|
||||||
dispatch_uid=request.request_id,
|
|
||||||
weak=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
def disconnect(self, request: HttpRequest):
|
def disconnect(self, request: HttpRequest):
|
||||||
"""Disconnect signals"""
|
"""Disconnect signals"""
|
||||||
@ -130,7 +109,6 @@ class AuditMiddleware:
|
|||||||
return
|
return
|
||||||
post_save.disconnect(dispatch_uid=request.request_id)
|
post_save.disconnect(dispatch_uid=request.request_id)
|
||||||
pre_delete.disconnect(dispatch_uid=request.request_id)
|
pre_delete.disconnect(dispatch_uid=request.request_id)
|
||||||
m2m_changed.disconnect(dispatch_uid=request.request_id)
|
|
||||||
|
|
||||||
def __call__(self, request: HttpRequest) -> HttpResponse:
|
def __call__(self, request: HttpRequest) -> HttpResponse:
|
||||||
self.connect(request)
|
self.connect(request)
|
||||||
@ -185,20 +163,3 @@ class AuditMiddleware:
|
|||||||
user=user,
|
user=user,
|
||||||
model=model_to_dict(instance),
|
model=model_to_dict(instance),
|
||||||
).run()
|
).run()
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def m2m_changed_handler(
|
|
||||||
user: User, request: HttpRequest, sender, instance: Model, action: str, **_
|
|
||||||
):
|
|
||||||
"""Signal handler for all object's m2m_changed"""
|
|
||||||
if action not in ["pre_add", "pre_remove", "post_clear"]:
|
|
||||||
return
|
|
||||||
if not should_log_m2m(instance):
|
|
||||||
return
|
|
||||||
|
|
||||||
EventNewThread(
|
|
||||||
EventAction.MODEL_UPDATED,
|
|
||||||
request,
|
|
||||||
user=user,
|
|
||||||
model=model_to_dict(instance),
|
|
||||||
).run()
|
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import uuid
|
import uuid
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
from typing import Iterable
|
||||||
|
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
from django.apps.registry import Apps
|
from django.apps.registry import Apps
|
||||||
@ -12,7 +13,6 @@ from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
|||||||
import authentik.events.models
|
import authentik.events.models
|
||||||
import authentik.lib.models
|
import authentik.lib.models
|
||||||
from authentik.events.models import EventAction, NotificationSeverity, TransportMode
|
from authentik.events.models import EventAction, NotificationSeverity, TransportMode
|
||||||
from authentik.lib.migrations import progress_bar
|
|
||||||
|
|
||||||
|
|
||||||
def convert_user_to_json(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
def convert_user_to_json(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||||
@ -43,6 +43,49 @@ def token_view_to_secret_view(apps: Apps, schema_editor: BaseDatabaseSchemaEdito
|
|||||||
Event.objects.using(db_alias).bulk_update(events, ["context", "action"])
|
Event.objects.using(db_alias).bulk_update(events, ["context", "action"])
|
||||||
|
|
||||||
|
|
||||||
|
# Taken from https://stackoverflow.com/questions/3173320/text-progress-bar-in-the-console
|
||||||
|
def progress_bar(
|
||||||
|
iterable: Iterable,
|
||||||
|
prefix="Writing: ",
|
||||||
|
suffix=" finished",
|
||||||
|
decimals=1,
|
||||||
|
length=100,
|
||||||
|
fill="█",
|
||||||
|
print_end="\r",
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Call in a loop to create terminal progress bar
|
||||||
|
@params:
|
||||||
|
iteration - Required : current iteration (Int)
|
||||||
|
total - Required : total iterations (Int)
|
||||||
|
prefix - Optional : prefix string (Str)
|
||||||
|
suffix - Optional : suffix string (Str)
|
||||||
|
decimals - Optional : positive number of decimals in percent complete (Int)
|
||||||
|
length - Optional : character length of bar (Int)
|
||||||
|
fill - Optional : bar fill character (Str)
|
||||||
|
print_end - Optional : end character (e.g. "\r", "\r\n") (Str)
|
||||||
|
"""
|
||||||
|
total = len(iterable)
|
||||||
|
if total < 1:
|
||||||
|
return
|
||||||
|
|
||||||
|
def print_progress_bar(iteration):
|
||||||
|
"""Progress Bar Printing Function"""
|
||||||
|
percent = ("{0:." + str(decimals) + "f}").format(100 * (iteration / float(total)))
|
||||||
|
filledLength = int(length * iteration // total)
|
||||||
|
bar = fill * filledLength + "-" * (length - filledLength)
|
||||||
|
print(f"\r{prefix} |{bar}| {percent}% {suffix}", end=print_end)
|
||||||
|
|
||||||
|
# Initial Call
|
||||||
|
print_progress_bar(0)
|
||||||
|
# Update Progress Bar
|
||||||
|
for i, item in enumerate(iterable):
|
||||||
|
yield item
|
||||||
|
print_progress_bar(i + 1)
|
||||||
|
# Print New Line on Complete
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
def update_expires(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
def update_expires(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||||
db_alias = schema_editor.connection.alias
|
db_alias = schema_editor.connection.alias
|
||||||
Event = apps.get_model("authentik_events", "event")
|
Event = apps.get_model("authentik_events", "event")
|
||||||
@ -57,6 +100,7 @@ def update_expires(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
replaces = [
|
replaces = [
|
||||||
("authentik_events", "0001_initial"),
|
("authentik_events", "0001_initial"),
|
||||||
("authentik_events", "0002_auto_20200918_2116"),
|
("authentik_events", "0002_auto_20200918_2116"),
|
||||||
@ -201,19 +245,14 @@ class Migration(migrations.Migration):
|
|||||||
models.TextField(
|
models.TextField(
|
||||||
choices=[("notice", "Notice"), ("warning", "Warning"), ("alert", "Alert")],
|
choices=[("notice", "Notice"), ("warning", "Warning"), ("alert", "Alert")],
|
||||||
default="notice",
|
default="notice",
|
||||||
help_text=(
|
help_text="Controls which severity level the created notifications will have.",
|
||||||
"Controls which severity level the created notifications will have."
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"group",
|
"group",
|
||||||
models.ForeignKey(
|
models.ForeignKey(
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text=(
|
help_text="Define which group of users this notification should be sent and shown to. If left empty, Notification won't ben sent.",
|
||||||
"Define which group of users this notification should be sent and shown"
|
|
||||||
" to. If left empty, Notification won't ben sent."
|
|
||||||
),
|
|
||||||
null=True,
|
null=True,
|
||||||
on_delete=django.db.models.deletion.SET_NULL,
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
to="authentik_core.group",
|
to="authentik_core.group",
|
||||||
@ -222,10 +261,7 @@ class Migration(migrations.Migration):
|
|||||||
(
|
(
|
||||||
"transports",
|
"transports",
|
||||||
models.ManyToManyField(
|
models.ManyToManyField(
|
||||||
help_text=(
|
help_text="Select which transports should be used to notify the user. If none are selected, the notification will only be shown in the authentik UI.",
|
||||||
"Select which transports should be used to notify the user. If none are"
|
|
||||||
" selected, the notification will only be shown in the authentik UI."
|
|
||||||
),
|
|
||||||
to="authentik_events.NotificationTransport",
|
to="authentik_events.NotificationTransport",
|
||||||
blank=True,
|
blank=True,
|
||||||
),
|
),
|
||||||
@ -281,10 +317,7 @@ class Migration(migrations.Migration):
|
|||||||
name="send_once",
|
name="send_once",
|
||||||
field=models.BooleanField(
|
field=models.BooleanField(
|
||||||
default=False,
|
default=False,
|
||||||
help_text=(
|
help_text="Only send notification once, for example when sending a webhook into a chat channel.",
|
||||||
"Only send notification once, for example when sending a webhook into a chat"
|
|
||||||
" channel."
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
migrations.RunPython(
|
migrations.RunPython(
|
||||||
|
@ -3,6 +3,7 @@ from django.db import migrations, models
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("authentik_events", "0001_squashed_0019_alter_notificationtransport_webhook_url"),
|
("authentik_events", "0001_squashed_0019_alter_notificationtransport_webhook_url"),
|
||||||
]
|
]
|
||||||
|
@ -283,6 +283,7 @@ class Event(SerializerModel, ExpiringModel):
|
|||||||
return f"Event action={self.action} user={self.user} context={self.context}"
|
return f"Event action={self.action} user={self.user} context={self.context}"
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
verbose_name = _("Event")
|
verbose_name = _("Event")
|
||||||
verbose_name_plural = _("Events")
|
verbose_name_plural = _("Events")
|
||||||
|
|
||||||
@ -361,9 +362,7 @@ class NotificationTransport(SerializerModel):
|
|||||||
)
|
)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
except RequestException as exc:
|
except RequestException as exc:
|
||||||
raise NotificationTransportError(
|
raise NotificationTransportError(exc.response.text) from exc
|
||||||
exc.response.text if exc.response else str(exc)
|
|
||||||
) from exc
|
|
||||||
return [
|
return [
|
||||||
response.status_code,
|
response.status_code,
|
||||||
response.text,
|
response.text,
|
||||||
@ -461,6 +460,7 @@ class NotificationTransport(SerializerModel):
|
|||||||
return f"Notification Transport {self.name}"
|
return f"Notification Transport {self.name}"
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
verbose_name = _("Notification Transport")
|
verbose_name = _("Notification Transport")
|
||||||
verbose_name_plural = _("Notification Transports")
|
verbose_name_plural = _("Notification Transports")
|
||||||
|
|
||||||
@ -495,6 +495,7 @@ class Notification(SerializerModel):
|
|||||||
return f"Notification for user {self.user}: {body_trunc}"
|
return f"Notification for user {self.user}: {body_trunc}"
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
verbose_name = _("Notification")
|
verbose_name = _("Notification")
|
||||||
verbose_name_plural = _("Notifications")
|
verbose_name_plural = _("Notifications")
|
||||||
|
|
||||||
@ -506,8 +507,10 @@ class NotificationRule(SerializerModel, PolicyBindingModel):
|
|||||||
transports = models.ManyToManyField(
|
transports = models.ManyToManyField(
|
||||||
NotificationTransport,
|
NotificationTransport,
|
||||||
help_text=_(
|
help_text=_(
|
||||||
"Select which transports should be used to notify the user. If none are "
|
(
|
||||||
"selected, the notification will only be shown in the authentik UI."
|
"Select which transports should be used to notify the user. If none are "
|
||||||
|
"selected, the notification will only be shown in the authentik UI."
|
||||||
|
)
|
||||||
),
|
),
|
||||||
blank=True,
|
blank=True,
|
||||||
)
|
)
|
||||||
@ -519,8 +522,10 @@ class NotificationRule(SerializerModel, PolicyBindingModel):
|
|||||||
group = models.ForeignKey(
|
group = models.ForeignKey(
|
||||||
Group,
|
Group,
|
||||||
help_text=_(
|
help_text=_(
|
||||||
"Define which group of users this notification should be sent and shown to. "
|
(
|
||||||
"If left empty, Notification won't ben sent."
|
"Define which group of users this notification should be sent and shown to. "
|
||||||
|
"If left empty, Notification won't ben sent."
|
||||||
|
)
|
||||||
),
|
),
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
@ -537,6 +542,7 @@ class NotificationRule(SerializerModel, PolicyBindingModel):
|
|||||||
return f"Notification Rule {self.name}"
|
return f"Notification Rule {self.name}"
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
verbose_name = _("Notification Rule")
|
verbose_name = _("Notification Rule")
|
||||||
verbose_name_plural = _("Notification Rules")
|
verbose_name_plural = _("Notification Rules")
|
||||||
|
|
||||||
@ -558,5 +564,6 @@ class NotificationWebhookMapping(PropertyMapping):
|
|||||||
return f"Webhook Mapping {self.name}"
|
return f"Webhook Mapping {self.name}"
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
verbose_name = _("Webhook Mapping")
|
verbose_name = _("Webhook Mapping")
|
||||||
verbose_name_plural = _("Webhook Mappings")
|
verbose_name_plural = _("Webhook Mappings")
|
||||||
|
@ -63,6 +63,11 @@ class TaskInfo:
|
|||||||
|
|
||||||
task_description: Optional[str] = field(default=None)
|
task_description: Optional[str] = field(default=None)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def html_name(self) -> list[str]:
|
||||||
|
"""Get task_name, but split on underscores, so we can join in the html template."""
|
||||||
|
return self.task_name.split("_")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def all() -> dict[str, "TaskInfo"]:
|
def all() -> dict[str, "TaskInfo"]:
|
||||||
"""Get all TaskInfo objects"""
|
"""Get all TaskInfo objects"""
|
||||||
@ -77,7 +82,7 @@ class TaskInfo:
|
|||||||
"""Delete task info from cache"""
|
"""Delete task info from cache"""
|
||||||
return cache.delete(CACHE_KEY_PREFIX + self.task_name)
|
return cache.delete(CACHE_KEY_PREFIX + self.task_name)
|
||||||
|
|
||||||
def update_metrics(self):
|
def set_prom_metrics(self):
|
||||||
"""Update prometheus metrics"""
|
"""Update prometheus metrics"""
|
||||||
start = default_timer()
|
start = default_timer()
|
||||||
if hasattr(self, "start_timestamp"):
|
if hasattr(self, "start_timestamp"):
|
||||||
@ -96,9 +101,9 @@ class TaskInfo:
|
|||||||
"""Save task into cache"""
|
"""Save task into cache"""
|
||||||
key = CACHE_KEY_PREFIX + self.task_name
|
key = CACHE_KEY_PREFIX + self.task_name
|
||||||
if self.result.uid:
|
if self.result.uid:
|
||||||
key += f":{self.result.uid}"
|
key += f"/{self.result.uid}"
|
||||||
self.task_name += f":{self.result.uid}"
|
self.task_name += f"/{self.result.uid}"
|
||||||
self.update_metrics()
|
self.set_prom_metrics()
|
||||||
cache.set(key, self, timeout=timeout_hours * 60 * 60)
|
cache.set(key, self, timeout=timeout_hours * 60 * 60)
|
||||||
|
|
||||||
|
|
||||||
@ -111,7 +116,6 @@ class MonitoredTask(Task):
|
|||||||
_result: Optional[TaskResult]
|
_result: Optional[TaskResult]
|
||||||
|
|
||||||
_uid: Optional[str]
|
_uid: Optional[str]
|
||||||
start: Optional[float] = None
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs) -> None:
|
def __init__(self, *args, **kwargs) -> None:
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
@ -119,6 +123,7 @@ class MonitoredTask(Task):
|
|||||||
self._uid = None
|
self._uid = None
|
||||||
self._result = None
|
self._result = None
|
||||||
self.result_timeout_hours = 6
|
self.result_timeout_hours = 6
|
||||||
|
self.start = default_timer()
|
||||||
|
|
||||||
def set_uid(self, uid: str):
|
def set_uid(self, uid: str):
|
||||||
"""Set UID, so in the case of an unexpected error its saved correctly"""
|
"""Set UID, so in the case of an unexpected error its saved correctly"""
|
||||||
@ -128,10 +133,6 @@ class MonitoredTask(Task):
|
|||||||
"""Set result for current run, will overwrite previous result."""
|
"""Set result for current run, will overwrite previous result."""
|
||||||
self._result = result
|
self._result = result
|
||||||
|
|
||||||
def before_start(self, task_id, args, kwargs):
|
|
||||||
self.start = default_timer()
|
|
||||||
return super().before_start(task_id, args, kwargs)
|
|
||||||
|
|
||||||
# pylint: disable=too-many-arguments
|
# pylint: disable=too-many-arguments
|
||||||
def after_return(self, status, retval, task_id, args: list[Any], kwargs: dict[str, Any], einfo):
|
def after_return(self, status, retval, task_id, args: list[Any], kwargs: dict[str, Any], einfo):
|
||||||
super().after_return(status, retval, task_id, args, kwargs, einfo=einfo)
|
super().after_return(status, retval, task_id, args, kwargs, einfo=einfo)
|
||||||
@ -142,7 +143,7 @@ class MonitoredTask(Task):
|
|||||||
info = TaskInfo(
|
info = TaskInfo(
|
||||||
task_name=self.__name__,
|
task_name=self.__name__,
|
||||||
task_description=self.__doc__,
|
task_description=self.__doc__,
|
||||||
start_timestamp=self.start or default_timer(),
|
start_timestamp=self.start,
|
||||||
finish_timestamp=default_timer(),
|
finish_timestamp=default_timer(),
|
||||||
finish_time=datetime.now(),
|
finish_time=datetime.now(),
|
||||||
result=self._result,
|
result=self._result,
|
||||||
@ -166,7 +167,7 @@ class MonitoredTask(Task):
|
|||||||
TaskInfo(
|
TaskInfo(
|
||||||
task_name=self.__name__,
|
task_name=self.__name__,
|
||||||
task_description=self.__doc__,
|
task_description=self.__doc__,
|
||||||
start_timestamp=self.start or default_timer(),
|
start_timestamp=self.start,
|
||||||
finish_timestamp=default_timer(),
|
finish_timestamp=default_timer(),
|
||||||
finish_time=datetime.now(),
|
finish_time=datetime.now(),
|
||||||
result=self._result,
|
result=self._result,
|
||||||
@ -177,7 +178,7 @@ class MonitoredTask(Task):
|
|||||||
).save(self.result_timeout_hours)
|
).save(self.result_timeout_hours)
|
||||||
Event.new(
|
Event.new(
|
||||||
EventAction.SYSTEM_TASK_EXCEPTION,
|
EventAction.SYSTEM_TASK_EXCEPTION,
|
||||||
message=f"Task {self.__name__} encountered an error: {exception_to_string(exc)}",
|
message=(f"Task {self.__name__} encountered an error: {exception_to_string(exc)}"),
|
||||||
).save()
|
).save()
|
||||||
|
|
||||||
def run(self, *args, **kwargs):
|
def run(self, *args, **kwargs):
|
||||||
|
@ -37,10 +37,11 @@ def event_notification_handler(event_uuid: str):
|
|||||||
@CELERY_APP.task()
|
@CELERY_APP.task()
|
||||||
def event_trigger_handler(event_uuid: str, trigger_name: str):
|
def event_trigger_handler(event_uuid: str, trigger_name: str):
|
||||||
"""Check if policies attached to NotificationRule match event"""
|
"""Check if policies attached to NotificationRule match event"""
|
||||||
event: Event = Event.objects.filter(event_uuid=event_uuid).first()
|
events = Event.objects.filter(event_uuid=event_uuid)
|
||||||
if not event:
|
if not events.exists():
|
||||||
LOGGER.warning("event doesn't exist yet or anymore", event_uuid=event_uuid)
|
LOGGER.warning("event doesn't exist yet or anymore", event_uuid=event_uuid)
|
||||||
return
|
return
|
||||||
|
event: Event = events.first()
|
||||||
trigger: Optional[NotificationRule] = NotificationRule.objects.filter(name=trigger_name).first()
|
trigger: Optional[NotificationRule] = NotificationRule.objects.filter(name=trigger_name).first()
|
||||||
if not trigger:
|
if not trigger:
|
||||||
return
|
return
|
||||||
|
@ -30,7 +30,7 @@ def cleanse_item(key: str, value: Any) -> Any:
|
|||||||
"""Cleanse a single item"""
|
"""Cleanse a single item"""
|
||||||
if isinstance(value, dict):
|
if isinstance(value, dict):
|
||||||
return cleanse_dict(value)
|
return cleanse_dict(value)
|
||||||
if isinstance(value, (list, tuple, set)):
|
if isinstance(value, list):
|
||||||
for idx, item in enumerate(value):
|
for idx, item in enumerate(value):
|
||||||
value[idx] = cleanse_item(key, item)
|
value[idx] = cleanse_item(key, item)
|
||||||
return value
|
return value
|
||||||
@ -103,7 +103,7 @@ def sanitize_item(value: Any) -> Any:
|
|||||||
return sanitize_dict(value)
|
return sanitize_dict(value)
|
||||||
if isinstance(value, GeneratorType):
|
if isinstance(value, GeneratorType):
|
||||||
return sanitize_item(list(value))
|
return sanitize_item(list(value))
|
||||||
if isinstance(value, (list, tuple, set)):
|
if isinstance(value, list):
|
||||||
new_values = []
|
new_values = []
|
||||||
for item in value:
|
for item in value:
|
||||||
new_value = sanitize_item(item)
|
new_value = sanitize_item(item)
|
||||||
|
@ -1,7 +1,4 @@
|
|||||||
"""Flow Binding API Views"""
|
"""Flow Binding API Views"""
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from rest_framework.exceptions import ValidationError
|
|
||||||
from rest_framework.serializers import ModelSerializer
|
from rest_framework.serializers import ModelSerializer
|
||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
@ -15,14 +12,8 @@ class FlowStageBindingSerializer(ModelSerializer):
|
|||||||
|
|
||||||
stage_obj = StageSerializer(read_only=True, source="stage")
|
stage_obj = StageSerializer(read_only=True, source="stage")
|
||||||
|
|
||||||
def validate(self, attrs: dict[str, Any]) -> dict[str, Any]:
|
|
||||||
evaluate_on_plan = attrs.get("evaluate_on_plan", False)
|
|
||||||
re_evaluate_policies = attrs.get("re_evaluate_policies", True)
|
|
||||||
if not evaluate_on_plan and not re_evaluate_policies:
|
|
||||||
raise ValidationError("Either evaluation on plan or evaluation on run must be enabled")
|
|
||||||
return super().validate(attrs)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
model = FlowStageBinding
|
model = FlowStageBinding
|
||||||
fields = [
|
fields = [
|
||||||
"pk",
|
"pk",
|
||||||
|
@ -53,6 +53,7 @@ class FlowSerializer(ModelSerializer):
|
|||||||
return reverse("authentik_api:flow-export", kwargs={"slug": flow.slug})
|
return reverse("authentik_api:flow-export", kwargs={"slug": flow.slug})
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
model = Flow
|
model = Flow
|
||||||
fields = [
|
fields = [
|
||||||
"pk",
|
"pk",
|
||||||
@ -81,6 +82,7 @@ class FlowSetSerializer(FlowSerializer):
|
|||||||
"""Stripped down flow serializer"""
|
"""Stripped down flow serializer"""
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
model = Flow
|
model = Flow
|
||||||
fields = [
|
fields = [
|
||||||
"pk",
|
"pk",
|
||||||
|
@ -8,7 +8,7 @@ from rest_framework.serializers import CharField
|
|||||||
|
|
||||||
from authentik.core.api.utils import PassiveSerializer
|
from authentik.core.api.utils import PassiveSerializer
|
||||||
from authentik.core.models import User
|
from authentik.core.models import User
|
||||||
from authentik.flows.models import Flow, FlowAuthenticationRequirement, FlowStageBinding
|
from authentik.flows.models import Flow, FlowStageBinding
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@ -160,37 +160,12 @@ class FlowDiagram:
|
|||||||
)
|
)
|
||||||
return stages + elements
|
return stages + elements
|
||||||
|
|
||||||
def get_flow_auth_requirement(self) -> list[DiagramElement]:
|
|
||||||
"""Get flow authentication requirement"""
|
|
||||||
end_el = DiagramElement(
|
|
||||||
"done",
|
|
||||||
_("End of the flow"),
|
|
||||||
_("Requirement not fulfilled"),
|
|
||||||
style=["[[", "]]"],
|
|
||||||
)
|
|
||||||
elements = []
|
|
||||||
if self.flow.authentication == FlowAuthenticationRequirement.NONE:
|
|
||||||
return []
|
|
||||||
auth = DiagramElement(
|
|
||||||
"flow_auth_requirement",
|
|
||||||
_("Flow authentication requirement") + "\n" + self.flow.authentication,
|
|
||||||
)
|
|
||||||
elements.append(auth)
|
|
||||||
end_el.source = [auth]
|
|
||||||
elements.append(end_el)
|
|
||||||
elements.append(
|
|
||||||
DiagramElement("flow_start", "placeholder", _("Requirement fulfilled"), source=[auth])
|
|
||||||
)
|
|
||||||
return elements
|
|
||||||
|
|
||||||
def build(self) -> str:
|
def build(self) -> str:
|
||||||
"""Build flowchart"""
|
"""Build flowchart"""
|
||||||
all_elements = [
|
all_elements = [
|
||||||
"graph TD",
|
"graph TD",
|
||||||
]
|
]
|
||||||
|
|
||||||
all_elements.extend(self.get_flow_auth_requirement())
|
|
||||||
|
|
||||||
pre_flow_policies_element = DiagramElement(
|
pre_flow_policies_element = DiagramElement(
|
||||||
"flow_pre", _("Pre-flow policies"), style=["[[", "]]"]
|
"flow_pre", _("Pre-flow policies"), style=["[[", "]]"]
|
||||||
)
|
)
|
||||||
@ -204,7 +179,6 @@ class FlowDiagram:
|
|||||||
_("End of the flow"),
|
_("End of the flow"),
|
||||||
_("Policy denied"),
|
_("Policy denied"),
|
||||||
flow_policies,
|
flow_policies,
|
||||||
style=["[[", "]]"],
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -33,6 +33,7 @@ class StageSerializer(ModelSerializer, MetaNameSerializer):
|
|||||||
return obj.component
|
return obj.component
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
model = Stage
|
model = Stage
|
||||||
fields = [
|
fields = [
|
||||||
"pk",
|
"pk",
|
||||||
|
@ -7,6 +7,7 @@ from django.db import migrations, models
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
replaces = [
|
replaces = [
|
||||||
("authentik_flows", "0001_initial"),
|
("authentik_flows", "0001_initial"),
|
||||||
("authentik_flows", "0003_auto_20200523_1133"),
|
("authentik_flows", "0003_auto_20200523_1133"),
|
||||||
@ -97,10 +98,7 @@ class Migration(migrations.Migration):
|
|||||||
"re_evaluate_policies",
|
"re_evaluate_policies",
|
||||||
models.BooleanField(
|
models.BooleanField(
|
||||||
default=False,
|
default=False,
|
||||||
help_text=(
|
help_text="When this option is enabled, the planner will re-evaluate policies bound to this.",
|
||||||
"When this option is enabled, the planner will re-evaluate policies"
|
|
||||||
" bound to this."
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
("order", models.IntegerField()),
|
("order", models.IntegerField()),
|
||||||
|
@ -4,6 +4,7 @@ from django.db import migrations
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("authentik_flows", "0007_auto_20200703_2059"),
|
("authentik_flows", "0007_auto_20200703_2059"),
|
||||||
]
|
]
|
||||||
|
@ -4,6 +4,7 @@ from django.db import migrations
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("authentik_flows", "0008_default_flows"),
|
("authentik_flows", "0008_default_flows"),
|
||||||
]
|
]
|
||||||
|
@ -4,6 +4,7 @@ from django.db import migrations
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("authentik_flows", "0009_source_flows"),
|
("authentik_flows", "0009_source_flows"),
|
||||||
]
|
]
|
||||||
|
@ -3,6 +3,7 @@ from django.db import migrations, models
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("authentik_flows", "0010_provider_flows"),
|
("authentik_flows", "0010_provider_flows"),
|
||||||
]
|
]
|
||||||
|
@ -20,6 +20,7 @@ def update_flow_designation(apps: Apps, schema_editor: BaseDatabaseSchemaEditor)
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
replaces = [
|
replaces = [
|
||||||
("authentik_flows", "0012_auto_20200908_1542"),
|
("authentik_flows", "0012_auto_20200908_1542"),
|
||||||
("authentik_flows", "0013_auto_20200924_1605"),
|
("authentik_flows", "0013_auto_20200924_1605"),
|
||||||
@ -78,10 +79,7 @@ class Migration(migrations.Migration):
|
|||||||
name="re_evaluate_policies",
|
name="re_evaluate_policies",
|
||||||
field=models.BooleanField(
|
field=models.BooleanField(
|
||||||
default=False,
|
default=False,
|
||||||
help_text=(
|
help_text="When this option is enabled, the planner will re-evaluate policies bound to this binding.",
|
||||||
"When this option is enabled, the planner will re-evaluate policies bound to"
|
|
||||||
" this binding."
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
@ -96,10 +94,7 @@ class Migration(migrations.Migration):
|
|||||||
name="evaluate_on_plan",
|
name="evaluate_on_plan",
|
||||||
field=models.BooleanField(
|
field=models.BooleanField(
|
||||||
default=True,
|
default=True,
|
||||||
help_text=(
|
help_text="Evaluate policies during the Flow planning process. Disable this for input-based policies.",
|
||||||
"Evaluate policies during the Flow planning process. Disable this for"
|
|
||||||
" input-based policies."
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
@ -125,10 +120,7 @@ class Migration(migrations.Migration):
|
|||||||
("recovery", "Recovery"),
|
("recovery", "Recovery"),
|
||||||
("stage_configuration", "Stage Configuration"),
|
("stage_configuration", "Stage Configuration"),
|
||||||
],
|
],
|
||||||
help_text=(
|
help_text="Decides what this Flow is used for. For example, the Authentication flow is redirect to when an un-authenticated user visits authentik.",
|
||||||
"Decides what this Flow is used for. For example, the Authentication flow is"
|
|
||||||
" redirect to when an un-authenticated user visits authentik."
|
|
||||||
),
|
|
||||||
max_length=100,
|
max_length=100,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -4,6 +4,7 @@ from django.db import migrations
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("authentik_flows", "0017_auto_20210329_1334"),
|
("authentik_flows", "0017_auto_20210329_1334"),
|
||||||
]
|
]
|
||||||
|
@ -4,6 +4,7 @@ from django.db import migrations, models
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
replaces = [
|
replaces = [
|
||||||
("authentik_flows", "0019_alter_flow_background"),
|
("authentik_flows", "0019_alter_flow_background"),
|
||||||
("authentik_flows", "0020_flow_compatibility_mode"),
|
("authentik_flows", "0020_flow_compatibility_mode"),
|
||||||
@ -38,12 +39,7 @@ class Migration(migrations.Migration):
|
|||||||
("restart_with_context", "Restart With Context"),
|
("restart_with_context", "Restart With Context"),
|
||||||
],
|
],
|
||||||
default="retry",
|
default="retry",
|
||||||
help_text=(
|
help_text="Configure how the flow executor should handle an invalid response to a challenge. RETRY returns the error message and a similar challenge to the executor. RESTART restarts the flow from the beginning, and RESTART_WITH_CONTEXT restarts the flow while keeping the current context.",
|
||||||
"Configure how the flow executor should handle an invalid response to a"
|
|
||||||
" challenge. RETRY returns the error message and a similar challenge to the"
|
|
||||||
" executor. RESTART restarts the flow from the beginning, and"
|
|
||||||
" RESTART_WITH_CONTEXT restarts the flow while keeping the current context."
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
@ -62,10 +58,7 @@ class Migration(migrations.Migration):
|
|||||||
name="compatibility_mode",
|
name="compatibility_mode",
|
||||||
field=models.BooleanField(
|
field=models.BooleanField(
|
||||||
default=False,
|
default=False,
|
||||||
help_text=(
|
help_text="Enable compatibility mode, increases compatibility with password managers on mobile devices.",
|
||||||
"Enable compatibility mode, increases compatibility with password managers on"
|
|
||||||
" mobile devices."
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
@ -5,6 +5,7 @@ from django.db import migrations, models
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("authentik_core", "0018_auto_20210330_1345_squashed_0028_alter_token_intent"),
|
("authentik_core", "0018_auto_20210330_1345_squashed_0028_alter_token_intent"),
|
||||||
(
|
(
|
||||||
|
@ -3,6 +3,7 @@ from django.db import migrations
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("authentik_flows", "0020_flowtoken"),
|
("authentik_flows", "0020_flowtoken"),
|
||||||
]
|
]
|
||||||
|
@ -4,6 +4,7 @@ from django.db import migrations, models
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("authentik_flows", "0021_auto_20211227_2103"),
|
("authentik_flows", "0021_auto_20211227_2103"),
|
||||||
]
|
]
|
||||||
|
@ -4,6 +4,7 @@ from django.db import migrations, models
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("authentik_flows", "0022_flow_layout"),
|
("authentik_flows", "0022_flow_layout"),
|
||||||
]
|
]
|
||||||
|
@ -4,6 +4,7 @@ from django.db import migrations, models
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("authentik_flows", "0023_flow_denied_action"),
|
("authentik_flows", "0023_flow_denied_action"),
|
||||||
]
|
]
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user