Compare commits
1 Commits
version/20
...
version-20
Author | SHA1 | Date | |
---|---|---|---|
fd7a7a6e64 |
@ -1,5 +1,5 @@
|
||||
[bumpversion]
|
||||
current_version = 2022.10.4
|
||||
current_version = 2022.8.2
|
||||
tag = True
|
||||
commit = True
|
||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)
|
||||
@ -17,4 +17,4 @@ tag_name = version/{new_version}
|
||||
|
||||
[bumpversion:file:internal/constants/constants.go]
|
||||
|
||||
[bumpversion:file:web/src/common/constants.ts]
|
||||
[bumpversion:file:web/src/constants.ts]
|
||||
|
@ -11,7 +11,38 @@ runs:
|
||||
steps:
|
||||
- name: Generate config
|
||||
id: ev
|
||||
uses: ./.github/actions/docker-push-variables
|
||||
shell: python
|
||||
run: |
|
||||
"""Helper script to get the actual branch name, docker safe"""
|
||||
import os
|
||||
from time import time
|
||||
|
||||
env_pr_branch = "GITHUB_HEAD_REF"
|
||||
default_branch = "GITHUB_REF"
|
||||
sha = "GITHUB_SHA"
|
||||
|
||||
branch_name = os.environ[default_branch]
|
||||
if os.environ.get(env_pr_branch, "") != "":
|
||||
branch_name = os.environ[env_pr_branch]
|
||||
|
||||
should_build = str(os.environ.get("DOCKER_USERNAME", "") != "").lower()
|
||||
|
||||
print("##[set-output name=branchName]%s" % branch_name)
|
||||
print(
|
||||
"##[set-output name=branchNameContainer]%s"
|
||||
% branch_name.replace("refs/heads/", "").replace("/", "-")
|
||||
)
|
||||
print("##[set-output name=timestamp]%s" % int(time()))
|
||||
print("##[set-output name=sha]%s" % os.environ[sha])
|
||||
print("##[set-output name=shouldBuild]%s" % should_build)
|
||||
|
||||
import configparser
|
||||
parser = configparser.ConfigParser()
|
||||
parser.read(".bumpversion.cfg")
|
||||
version = parser.get("bumpversion", "current_version")
|
||||
version_family = ".".join(version.split(".")[:-1])
|
||||
print("##[set-output name=version]%s" % version)
|
||||
print("##[set-output name=versionFamily]%s" % version_family)
|
||||
- name: Find Comment
|
||||
uses: peter-evans/find-comment@v2
|
||||
id: fc
|
||||
@ -52,6 +83,8 @@ runs:
|
||||
image:
|
||||
repository: ghcr.io/goauthentik/dev-server
|
||||
tag: ${{ inputs.tag }}
|
||||
# pullPolicy: Always to ensure you always get the latest version
|
||||
pullPolicy: Always
|
||||
```
|
||||
|
||||
Afterwards, run the upgrade commands from the latest release notes.
|
||||
|
37
.github/actions/docker-push-variables/action.yml
vendored
37
.github/actions/docker-push-variables/action.yml
vendored
@ -32,27 +32,32 @@ runs:
|
||||
shell: python
|
||||
run: |
|
||||
"""Helper script to get the actual branch name, docker safe"""
|
||||
import configparser
|
||||
import os
|
||||
from time import time
|
||||
|
||||
parser = configparser.ConfigParser()
|
||||
parser.read(".bumpversion.cfg")
|
||||
env_pr_branch = "GITHUB_HEAD_REF"
|
||||
default_branch = "GITHUB_REF"
|
||||
sha = "GITHUB_SHA"
|
||||
|
||||
branch_name = os.environ["GITHUB_REF"]
|
||||
if os.environ.get("GITHUB_HEAD_REF", "") != "":
|
||||
branch_name = os.environ["GITHUB_HEAD_REF"]
|
||||
branch_name = os.environ[default_branch]
|
||||
if os.environ.get(env_pr_branch, "") != "":
|
||||
branch_name = os.environ[env_pr_branch]
|
||||
|
||||
should_build = str(os.environ.get("DOCKER_USERNAME", "") != "").lower()
|
||||
|
||||
print("##[set-output name=branchName]%s" % branch_name)
|
||||
print(
|
||||
"##[set-output name=branchNameContainer]%s"
|
||||
% branch_name.replace("refs/heads/", "").replace("/", "-")
|
||||
)
|
||||
print("##[set-output name=timestamp]%s" % int(time()))
|
||||
print("##[set-output name=sha]%s" % os.environ[sha])
|
||||
print("##[set-output name=shouldBuild]%s" % should_build)
|
||||
|
||||
import configparser
|
||||
parser = configparser.ConfigParser()
|
||||
parser.read(".bumpversion.cfg")
|
||||
version = parser.get("bumpversion", "current_version")
|
||||
version_family = ".".join(version.split(".")[:-1])
|
||||
safe_branch_name = branch_name.replace("refs/heads/", "").replace("/", "-")
|
||||
|
||||
with open(os.environ["GITHUB_OUTPUT"], "a+", encoding="utf-8") as _output:
|
||||
print("branchName=%s" % branch_name, file=_output)
|
||||
print("branchNameContainer=%s" % safe_branch_name, file=_output)
|
||||
print("timestamp=%s" % int(time()), file=_output)
|
||||
print("sha=%s" % os.environ["GITHUB_SHA"], file=_output)
|
||||
print("shouldBuild=%s" % should_build, file=_output)
|
||||
print("version=%s" % version, file=_output)
|
||||
print("versionFamily=%s" % version_family, file=_output)
|
||||
print("##[set-output name=version]%s" % version)
|
||||
print("##[set-output name=versionFamily]%s" % version_family)
|
||||
|
6
.github/actions/setup/action.yml
vendored
6
.github/actions/setup/action.yml
vendored
@ -1,5 +1,5 @@
|
||||
name: 'Setup authentik testing environment'
|
||||
description: 'Setup authentik testing environment'
|
||||
name: 'Setup authentik testing environemnt'
|
||||
description: 'Setup authentik testing environemnt'
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
@ -27,7 +27,7 @@ runs:
|
||||
docker-compose -f .github/actions/setup/docker-compose.yml up -d
|
||||
poetry env use python3.10
|
||||
poetry install
|
||||
cd web && npm ci
|
||||
npm install -g pyright@1.1.136
|
||||
- name: Generate config
|
||||
shell: poetry run python {0}
|
||||
run: |
|
||||
|
1
.github/codespell-words.txt
vendored
1
.github/codespell-words.txt
vendored
@ -1,4 +1,3 @@
|
||||
keypair
|
||||
keypairs
|
||||
hass
|
||||
warmup
|
||||
|
16
.github/transifex.yml
vendored
16
.github/transifex.yml
vendored
@ -1,16 +0,0 @@
|
||||
git:
|
||||
filters:
|
||||
- filter_type: file
|
||||
# all supported i18n types: https://docs.transifex.com/formats
|
||||
file_format: PO
|
||||
source_language: en
|
||||
source_file: web/src/locales/en.po
|
||||
# path expression to translation files, must contain <lang> placeholder
|
||||
translation_files_expression: 'web/src/locales/<lang>.po'
|
||||
- filter_type: file
|
||||
# all supported i18n types: https://docs.transifex.com/formats
|
||||
file_format: PO
|
||||
source_language: en
|
||||
source_file: locale/en/LC_MESSAGES/django.po
|
||||
# path expression to translation files, must contain <lang> placeholder
|
||||
translation_files_expression: 'locale/<lang>/LC_MESSAGES/django.po'
|
18
.github/workflows/ci-main.yml
vendored
18
.github/workflows/ci-main.yml
vendored
@ -96,8 +96,6 @@ jobs:
|
||||
testspace [unittest]unittest.xml --link=codecov
|
||||
- if: ${{ always() }}
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
flags: unit
|
||||
test-integration:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@ -108,7 +106,7 @@ jobs:
|
||||
with:
|
||||
domain: ${{github.repository_owner}}
|
||||
- name: Create k8s Kind Cluster
|
||||
uses: helm/kind-action@v1.4.0
|
||||
uses: helm/kind-action@v1.3.0
|
||||
- name: run integration
|
||||
run: |
|
||||
poetry run make test-integration
|
||||
@ -119,8 +117,6 @@ jobs:
|
||||
testspace [integration]unittest.xml --link=codecov
|
||||
- if: ${{ always() }}
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
flags: integration
|
||||
test-e2e-provider:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@ -130,7 +126,7 @@ jobs:
|
||||
- uses: testspace-com/setup-testspace@v1
|
||||
with:
|
||||
domain: ${{github.repository_owner}}
|
||||
- name: Setup e2e env (chrome, etc)
|
||||
- name: Setup authentik env
|
||||
run: |
|
||||
docker-compose -f tests/e2e/docker-compose.yml up -d
|
||||
- id: cache-web
|
||||
@ -155,8 +151,6 @@ jobs:
|
||||
testspace [e2e-provider]unittest.xml --link=codecov
|
||||
- if: ${{ always() }}
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
flags: e2e
|
||||
test-e2e-rest:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@ -166,7 +160,7 @@ jobs:
|
||||
- uses: testspace-com/setup-testspace@v1
|
||||
with:
|
||||
domain: ${{github.repository_owner}}
|
||||
- name: Setup e2e env (chrome, etc)
|
||||
- name: Setup authentik env
|
||||
run: |
|
||||
docker-compose -f tests/e2e/docker-compose.yml up -d
|
||||
- id: cache-web
|
||||
@ -191,8 +185,6 @@ jobs:
|
||||
testspace [e2e-rest]unittest.xml --link=codecov
|
||||
- if: ${{ always() }}
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
flags: e2e
|
||||
ci-core-mark:
|
||||
needs:
|
||||
- lint
|
||||
@ -217,7 +209,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2.1.0
|
||||
uses: docker/setup-qemu-action@v2.0.0
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
- name: prepare variables
|
||||
@ -248,4 +240,4 @@ jobs:
|
||||
continue-on-error: true
|
||||
uses: ./.github/actions/comment-pr-instructions
|
||||
with:
|
||||
tag: gh-${{ steps.ev.outputs.branchNameContainer }}-${{ steps.ev.outputs.timestamp }}-${{ steps.ev.outputs.sha }}
|
||||
tag: gh-${{ steps.ev.outputs.branchNameContainer }}
|
||||
|
4
.github/workflows/ci-outpost.yml
vendored
4
.github/workflows/ci-outpost.yml
vendored
@ -63,7 +63,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2.1.0
|
||||
uses: docker/setup-qemu-action@v2.0.0
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
- name: prepare variables
|
||||
@ -111,7 +111,7 @@ jobs:
|
||||
- uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: "^1.17"
|
||||
- uses: actions/setup-node@v3.5.1
|
||||
- uses: actions/setup-node@v3.4.1
|
||||
with:
|
||||
node-version: '16'
|
||||
cache: 'npm'
|
||||
|
8
.github/workflows/ci-web.yml
vendored
8
.github/workflows/ci-web.yml
vendored
@ -15,7 +15,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3.5.1
|
||||
- uses: actions/setup-node@v3.4.1
|
||||
with:
|
||||
node-version: '16'
|
||||
cache: 'npm'
|
||||
@ -31,7 +31,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3.5.1
|
||||
- uses: actions/setup-node@v3.4.1
|
||||
with:
|
||||
node-version: '16'
|
||||
cache: 'npm'
|
||||
@ -47,7 +47,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3.5.1
|
||||
- uses: actions/setup-node@v3.4.1
|
||||
with:
|
||||
node-version: '16'
|
||||
cache: 'npm'
|
||||
@ -78,7 +78,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3.5.1
|
||||
- uses: actions/setup-node@v3.4.1
|
||||
with:
|
||||
node-version: '16'
|
||||
cache: 'npm'
|
||||
|
2
.github/workflows/ci-website.yml
vendored
2
.github/workflows/ci-website.yml
vendored
@ -15,7 +15,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3.5.1
|
||||
- uses: actions/setup-node@v3.4.1
|
||||
with:
|
||||
node-version: '16'
|
||||
cache: 'npm'
|
||||
|
6
.github/workflows/release-publish.yml
vendored
6
.github/workflows/release-publish.yml
vendored
@ -10,7 +10,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2.1.0
|
||||
uses: docker/setup-qemu-action@v2.0.0
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
- name: prepare variables
|
||||
@ -54,7 +54,7 @@ jobs:
|
||||
with:
|
||||
go-version: "^1.17"
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2.1.0
|
||||
uses: docker/setup-qemu-action@v2.0.0
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
- name: prepare variables
|
||||
@ -100,7 +100,7 @@ jobs:
|
||||
- uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: "^1.17"
|
||||
- uses: actions/setup-node@v3.5.1
|
||||
- uses: actions/setup-node@v3.4.1
|
||||
with:
|
||||
node-version: '16'
|
||||
cache: 'npm'
|
||||
|
3
.github/workflows/web-api-publish.yml
vendored
3
.github/workflows/web-api-publish.yml
vendored
@ -10,7 +10,8 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3.5.1
|
||||
# Setup .npmrc file to publish to npm
|
||||
- uses: actions/setup-node@v3.4.1
|
||||
with:
|
||||
node-version: '16'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
@ -17,24 +17,24 @@ diverse, inclusive, and healthy community.
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
- Demonstrating empathy and kindness toward other people
|
||||
- Being respectful of differing opinions, viewpoints, and experiences
|
||||
- Giving and gracefully accepting constructive feedback
|
||||
- Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
- Focusing on what is best not just for us as individuals, but for the
|
||||
overall community
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the
|
||||
overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
- The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
- Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
- Public or private harassment
|
||||
- Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
- Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
* The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
@ -106,7 +106,7 @@ Violating these terms may lead to a permanent ban.
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within
|
||||
|
@ -11,22 +11,19 @@ The following is a set of guidelines for contributing to authentik and its compo
|
||||
[I don't want to read this whole thing, I just have a question!!!](#i-dont-want-to-read-this-whole-thing-i-just-have-a-question)
|
||||
|
||||
[What should I know before I get started?](#what-should-i-know-before-i-get-started)
|
||||
|
||||
- [The components](#the-components)
|
||||
- [authentik's structure](#authentiks-structure)
|
||||
* [The components](#the-components)
|
||||
* [authentik's structure](#authentiks-structure)
|
||||
|
||||
[How Can I Contribute?](#how-can-i-contribute)
|
||||
|
||||
- [Reporting Bugs](#reporting-bugs)
|
||||
- [Suggesting Enhancements](#suggesting-enhancements)
|
||||
- [Your First Code Contribution](#your-first-code-contribution)
|
||||
- [Pull Requests](#pull-requests)
|
||||
* [Reporting Bugs](#reporting-bugs)
|
||||
* [Suggesting Enhancements](#suggesting-enhancements)
|
||||
* [Your First Code Contribution](#your-first-code-contribution)
|
||||
* [Pull Requests](#pull-requests)
|
||||
|
||||
[Styleguides](#styleguides)
|
||||
|
||||
- [Git Commit Messages](#git-commit-messages)
|
||||
- [Python Styleguide](#python-styleguide)
|
||||
- [Documentation Styleguide](#documentation-styleguide)
|
||||
* [Git Commit Messages](#git-commit-messages)
|
||||
* [Python Styleguide](#python-styleguide)
|
||||
* [Documentation Styleguide](#documentation-styleguide)
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
@ -42,11 +39,11 @@ Either [create a question on GitHub](https://github.com/goauthentik/authentik/is
|
||||
|
||||
authentik consists of a few larger components:
|
||||
|
||||
- _authentik_ the actual application server, is described below.
|
||||
- _outpost-proxy_ is a Go application based on a forked version of oauth2_proxy, which does identity-aware reverse proxying.
|
||||
- _outpost-ldap_ is a Go LDAP server that uses the _authentik_ application server as its backend
|
||||
- _web_ is the web frontend, both for administrating and using authentik. It is written in TypeScript using lit-html and the PatternFly CSS Library.
|
||||
- _website_ is the Website/documentation, which uses docusaurus.
|
||||
- *authentik* the actual application server, is described below.
|
||||
- *outpost-proxy* is a Go application based on a forked version of oauth2_proxy, which does identity-aware reverse proxying.
|
||||
- *outpost-ldap* is a Go LDAP server that uses the *authentik* application server as its backend
|
||||
- *web* is the web frontend, both for administrating and using authentik. It is written in TypeScript using lit-html and the PatternFly CSS Library.
|
||||
- *website* is the Website/documentation, which uses docusaurus.
|
||||
|
||||
### authentik's structure
|
||||
|
||||
@ -140,10 +137,10 @@ This is documented in the [developer docs](https://goauthentik.io/developer-docs
|
||||
|
||||
The process described here has several goals:
|
||||
|
||||
- Maintain authentik's quality
|
||||
- Fix problems that are important to users
|
||||
- Engage the community in working toward the best possible authentik
|
||||
- Enable a sustainable system for authentik's maintainers to review contributions
|
||||
- Maintain authentik's quality
|
||||
- Fix problems that are important to users
|
||||
- Engage the community in working toward the best possible authentik
|
||||
- Enable a sustainable system for authentik's maintainers to review contributions
|
||||
|
||||
Please follow these steps to have your contribution considered by the maintainers:
|
||||
|
||||
@ -157,10 +154,10 @@ While the prerequisites above must be satisfied prior to having your pull reques
|
||||
|
||||
### Git Commit Messages
|
||||
|
||||
- Use the format of `<package>: <verb> <description>`
|
||||
- See [here](#authentik-packages) for `package`
|
||||
- Example: `providers/saml2: fix parsing of requests`
|
||||
- Reference issues and pull requests liberally after the first line
|
||||
* Use the format of `<package>: <verb> <description>`
|
||||
- See [here](#authentik-packages) for `package`
|
||||
- Example: `providers/saml2: fix parsing of requests`
|
||||
* Reference issues and pull requests liberally after the first line
|
||||
|
||||
### Python Styleguide
|
||||
|
||||
@ -168,11 +165,11 @@ All Python code is linted with [black](https://black.readthedocs.io/en/stable/),
|
||||
|
||||
authentik runs on Python 3.9 at the time of writing this.
|
||||
|
||||
- Use native type-annotations wherever possible.
|
||||
- Add meaningful docstrings when possible.
|
||||
- Ensure any database migrations work properly from the last stable version (this is checked via CI)
|
||||
- If your code changes central functions, make sure nothing else is broken.
|
||||
* Use native type-annotations wherever possible.
|
||||
* Add meaningful docstrings when possible.
|
||||
* Ensure any database migrations work properly from the last stable version (this is checked via CI)
|
||||
* If your code changes central functions, make sure nothing else is broken.
|
||||
|
||||
### Documentation Styleguide
|
||||
|
||||
- Use [MDX](https://mdxjs.com/) whenever appropriate.
|
||||
* Use [MDX](https://mdxjs.com/) whenever appropriate.
|
||||
|
@ -3,7 +3,6 @@ FROM --platform=${BUILDPLATFORM} docker.io/node:18 as website-builder
|
||||
|
||||
COPY ./website /work/website/
|
||||
COPY ./blueprints /work/blueprints/
|
||||
COPY ./SECURITY.md /work/
|
||||
|
||||
ENV NODE_ENV=production
|
||||
WORKDIR /work/website
|
||||
@ -20,7 +19,7 @@ WORKDIR /work/web
|
||||
RUN npm ci && npm run build
|
||||
|
||||
# Stage 3: Poetry to requirements.txt export
|
||||
FROM docker.io/python:3.10.7-slim-bullseye AS poetry-locker
|
||||
FROM docker.io/python:3.10.6-slim-bullseye AS poetry-locker
|
||||
|
||||
WORKDIR /work
|
||||
COPY ./pyproject.toml /work
|
||||
@ -31,7 +30,7 @@ RUN pip install --no-cache-dir poetry && \
|
||||
poetry export -f requirements.txt --dev --output requirements-dev.txt
|
||||
|
||||
# Stage 4: Build go proxy
|
||||
FROM docker.io/golang:1.19.2-bullseye AS go-builder
|
||||
FROM docker.io/golang:1.19.0-bullseye AS go-builder
|
||||
|
||||
WORKDIR /work
|
||||
|
||||
@ -44,10 +43,10 @@ COPY ./internal /work/internal
|
||||
COPY ./go.mod /work/go.mod
|
||||
COPY ./go.sum /work/go.sum
|
||||
|
||||
RUN go build -o /work/authentik ./cmd/server/
|
||||
RUN go build -o /work/authentik ./cmd/server/main.go
|
||||
|
||||
# Stage 5: Run
|
||||
FROM docker.io/python:3.10.7-slim-bullseye AS final-image
|
||||
FROM docker.io/python:3.10.6-slim-bullseye AS final-image
|
||||
|
||||
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.
|
||||
|
29
Makefile
29
Makefile
@ -28,7 +28,7 @@ test-docker:
|
||||
rm -f .env
|
||||
|
||||
test:
|
||||
coverage run manage.py test --keepdb authentik
|
||||
coverage run manage.py test authentik
|
||||
coverage html
|
||||
coverage report
|
||||
|
||||
@ -53,7 +53,7 @@ migrate:
|
||||
python -m lifecycle.migrate
|
||||
|
||||
run:
|
||||
go run -v ./cmd/server/
|
||||
go run -v cmd/server/main.go
|
||||
|
||||
i18n-extract: i18n-extract-core web-extract
|
||||
|
||||
@ -73,7 +73,7 @@ gen-diff:
|
||||
docker run \
|
||||
--rm -v ${PWD}:/local \
|
||||
--user ${UID}:${GID} \
|
||||
docker.io/openapitools/openapi-diff:2.1.0-beta.3 \
|
||||
docker.io/openapitools/openapi-diff:2.0.1 \
|
||||
--markdown /local/diff.md \
|
||||
/local/old_schema.yml /local/schema.yml
|
||||
rm old_schema.yml
|
||||
@ -90,11 +90,9 @@ gen-client-ts:
|
||||
-i /local/schema.yml \
|
||||
-g typescript-fetch \
|
||||
-o /local/gen-ts-api \
|
||||
-c /local/scripts/api-ts-config.yaml \
|
||||
--additional-properties=npmVersion=${NPM_VERSION} \
|
||||
--git-repo-id authentik \
|
||||
--git-user-id goauthentik
|
||||
--additional-properties=typescriptThreePlus=true,supportsES6=true,npmName=@goauthentik/api,npmVersion=${NPM_VERSION}
|
||||
mkdir -p web/node_modules/@goauthentik/api
|
||||
\cp -fv scripts/web_api_readme.md gen-ts-api/README.md
|
||||
cd gen-ts-api && npm i
|
||||
\cp -rfv gen-ts-api/* web/node_modules/@goauthentik/api
|
||||
|
||||
@ -151,7 +149,7 @@ web-extract:
|
||||
## Website
|
||||
#########################
|
||||
|
||||
website: website-lint-fix website-build
|
||||
website: website-lint-fix
|
||||
|
||||
website-install:
|
||||
cd website && npm ci
|
||||
@ -159,33 +157,30 @@ website-install:
|
||||
website-lint-fix:
|
||||
cd website && npm run prettier
|
||||
|
||||
website-build:
|
||||
cd website && npm run build
|
||||
|
||||
website-watch:
|
||||
cd website && npm run watch
|
||||
|
||||
# These targets are use by GitHub actions to allow usage of matrix
|
||||
# which makes the YAML File a lot smaller
|
||||
PY_SOURCES=authentik tests lifecycle
|
||||
|
||||
ci--meta-debug:
|
||||
python -V
|
||||
node --version
|
||||
|
||||
ci-pylint: ci--meta-debug
|
||||
pylint $(PY_SOURCES)
|
||||
pylint authentik tests lifecycle
|
||||
|
||||
ci-black: ci--meta-debug
|
||||
black --check $(PY_SOURCES)
|
||||
black --check authentik tests lifecycle
|
||||
|
||||
ci-isort: ci--meta-debug
|
||||
isort --check $(PY_SOURCES)
|
||||
isort --check authentik tests lifecycle
|
||||
|
||||
ci-bandit: ci--meta-debug
|
||||
bandit -r $(PY_SOURCES)
|
||||
bandit -r authentik tests lifecycle
|
||||
|
||||
ci-pyright: ci--meta-debug
|
||||
./web/node_modules/.bin/pyright $(PY_SOURCES)
|
||||
pyright e2e lifecycle
|
||||
|
||||
ci-pending-migrations: ci--meta-debug
|
||||
ak makemigrations --check
|
||||
|
@ -26,10 +26,10 @@ For bigger setups, there is a Helm Chart [here](https://github.com/goauthentik/h
|
||||
|
||||
## Screenshots
|
||||
|
||||
| Light | Dark |
|
||||
| ------------------------------------------------------ | ----------------------------------------------------- |
|
||||
|  |  |
|
||||
|  |  |
|
||||
Light | Dark
|
||||
--- | ---
|
||||
 | 
|
||||
 | 
|
||||
|
||||
## Development
|
||||
|
||||
|
43
SECURITY.md
43
SECURITY.md
@ -1,44 +1,15 @@
|
||||
Authentik takes security very seriously. We follow the rules of [responsible disclosure](https://en.wikipedia.org/wiki/Responsible_disclosure), and we urge our community to do so as well, instead of reporting vulnerabilities publicly. This allows us to patch the issue quickly, announce it's existence and release the fixed version.
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
(.x being the latest patch release for each version)
|
||||
|
||||
| Version | Supported |
|
||||
| --------- | ------------------ |
|
||||
| 2022.10.x | :white_check_mark: |
|
||||
| 2022.11.x | :white_check_mark: |
|
||||
| Version | Supported |
|
||||
| ---------- | ------------------ |
|
||||
| 2022.6.x | :white_check_mark: |
|
||||
| 2022.7.x | :white_check_mark: |
|
||||
| 2022.8.x | :white_check_mark: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
To report a vulnerability, send an email to [security@goauthentik.io](mailto:security@goauthentik.io). Be sure to include relevant information like which version you've found the issue in, instructions on how to reproduce the issue, and anything else that might make it easier for us to find the bug.
|
||||
|
||||
## Criticality levels
|
||||
|
||||
### High
|
||||
|
||||
- Authorization bypass
|
||||
- Circumvention of policies
|
||||
|
||||
### Moderate
|
||||
|
||||
- Denial-of-Service attacks
|
||||
|
||||
### Low
|
||||
|
||||
- Unvalidated redirects
|
||||
- Issues requiring uncommon setups
|
||||
|
||||
## Disclosure process
|
||||
|
||||
1. Issue is reported via Email as listed above.
|
||||
2. The authentik Security team will try to reproduce the issue and ask for more information if required.
|
||||
3. A criticality level is assigned.
|
||||
4. A fix is created, and if possible tested by the issue reporter.
|
||||
5. The fix is backported to other supported versions, and if possible a workaround for other versions is created.
|
||||
6. An announcement is sent out with a fixed release date and criticality level of the issue. The announcement will be sent at least 24 hours before the release of the fix
|
||||
7. The fixed version is released for the supported versions.
|
||||
|
||||
## Getting security notifications
|
||||
|
||||
To get security notifications, join the [discord](https://goauthentik.io/discord) server. In the future there will be a mailing list too.
|
||||
To report a vulnerability, send an email to [security@goauthentik.io](mailto:security@goauthentik.io)
|
||||
|
@ -2,7 +2,7 @@
|
||||
from os import environ
|
||||
from typing import Optional
|
||||
|
||||
__version__ = "2022.10.4"
|
||||
__version__ = "2022.8.2"
|
||||
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
||||
|
||||
|
||||
|
@ -23,7 +23,6 @@ class LoginMetricsSerializer(PassiveSerializer):
|
||||
|
||||
logins_per_1h = SerializerMethodField()
|
||||
logins_failed_per_1h = SerializerMethodField()
|
||||
authorizations_per_1h = SerializerMethodField()
|
||||
|
||||
@extend_schema_field(CoordinateSerializer(many=True))
|
||||
def get_logins_per_1h(self, _):
|
||||
@ -45,16 +44,6 @@ class LoginMetricsSerializer(PassiveSerializer):
|
||||
.get_events_per_hour()
|
||||
)
|
||||
|
||||
@extend_schema_field(CoordinateSerializer(many=True))
|
||||
def get_authorizations_per_1h(self, _):
|
||||
"""Get successful authorizations per hour for the last 24 hours"""
|
||||
user = self.context["user"]
|
||||
return (
|
||||
get_objects_for_user(user, "authentik_events.view_event")
|
||||
.filter(action=EventAction.AUTHORIZE_APPLICATION)
|
||||
.get_events_per_hour()
|
||||
)
|
||||
|
||||
|
||||
class AdministrationMetricsViewSet(APIView):
|
||||
"""Login Metrics per 1h"""
|
||||
|
@ -5,7 +5,7 @@ from django.contrib import messages
|
||||
from django.http.response import Http404
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema
|
||||
from drf_spectacular.utils import OpenApiResponse, extend_schema
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.fields import CharField, ChoiceField, DateTimeField, ListField
|
||||
from rest_framework.permissions import IsAdminUser
|
||||
@ -58,15 +58,7 @@ class TaskViewSet(ViewSet):
|
||||
responses={
|
||||
200: TaskSerializer(many=False),
|
||||
404: OpenApiResponse(description="Task not found"),
|
||||
},
|
||||
parameters=[
|
||||
OpenApiParameter(
|
||||
"id",
|
||||
type=OpenApiTypes.STR,
|
||||
location=OpenApiParameter.PATH,
|
||||
required=True,
|
||||
),
|
||||
],
|
||||
}
|
||||
)
|
||||
# pylint: disable=invalid-name
|
||||
def retrieve(self, request: Request, pk=None) -> Response:
|
||||
@ -89,14 +81,6 @@ class TaskViewSet(ViewSet):
|
||||
404: OpenApiResponse(description="Task not found"),
|
||||
500: OpenApiResponse(description="Failed to retry task"),
|
||||
},
|
||||
parameters=[
|
||||
OpenApiParameter(
|
||||
"id",
|
||||
type=OpenApiTypes.STR,
|
||||
location=OpenApiParameter.PATH,
|
||||
required=True,
|
||||
),
|
||||
],
|
||||
)
|
||||
@action(detail=True, methods=["post"])
|
||||
# pylint: disable=invalid-name
|
||||
|
@ -1,7 +1,7 @@
|
||||
"""authentik admin app config"""
|
||||
from prometheus_client import Gauge, Info
|
||||
|
||||
from authentik.blueprints.apps import ManagedAppConfig
|
||||
from authentik.blueprints.manager import ManagedAppConfig
|
||||
|
||||
PROM_INFO = Info("authentik_version", "Currently running authentik version")
|
||||
GAUGE_WORKERS = Gauge("authentik_admin_workers", "Currently connected workers")
|
||||
|
@ -16,7 +16,7 @@ from authentik.providers.oauth2.models import RefreshToken
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
def validate_auth(header: bytes) -> Optional[str]:
|
||||
def validate_auth(header: bytes) -> str:
|
||||
"""Validate that the header is in a correct format,
|
||||
returns type and credentials"""
|
||||
auth_credentials = header.decode().strip()
|
||||
|
@ -28,7 +28,6 @@ class Capabilities(models.TextChoices):
|
||||
CAN_SAVE_MEDIA = "can_save_media"
|
||||
CAN_GEO_IP = "can_geo_ip"
|
||||
CAN_IMPERSONATE = "can_impersonate"
|
||||
CAN_DEBUG = "can_debug"
|
||||
|
||||
|
||||
class ErrorReportingConfigSerializer(PassiveSerializer):
|
||||
@ -67,8 +66,6 @@ class ConfigView(APIView):
|
||||
caps.append(Capabilities.CAN_GEO_IP)
|
||||
if CONFIG.y_bool("impersonation"):
|
||||
caps.append(Capabilities.CAN_IMPERSONATE)
|
||||
if settings.DEBUG:
|
||||
caps.append(Capabilities.CAN_DEBUG)
|
||||
return caps
|
||||
|
||||
def get_config(self) -> ConfigSerializer:
|
||||
|
@ -59,8 +59,7 @@ from authentik.sources.oauth.api.source import OAuthSourceViewSet
|
||||
from authentik.sources.oauth.api.source_connection import UserOAuthSourceConnectionViewSet
|
||||
from authentik.sources.plex.api.source import PlexSourceViewSet
|
||||
from authentik.sources.plex.api.source_connection import PlexSourceConnectionViewSet
|
||||
from authentik.sources.saml.api.source import SAMLSourceViewSet
|
||||
from authentik.sources.saml.api.source_connection import UserSAMLSourceConnectionViewSet
|
||||
from authentik.sources.saml.api import SAMLSourceViewSet
|
||||
from authentik.stages.authenticator_duo.api import (
|
||||
AuthenticatorDuoStageViewSet,
|
||||
DuoAdminDeviceViewSet,
|
||||
@ -139,7 +138,6 @@ router.register("sources/all", SourceViewSet)
|
||||
router.register("sources/user_connections/all", UserSourceConnectionViewSet)
|
||||
router.register("sources/user_connections/oauth", UserOAuthSourceConnectionViewSet)
|
||||
router.register("sources/user_connections/plex", PlexSourceConnectionViewSet)
|
||||
router.register("sources/user_connections/saml", UserSAMLSourceConnectionViewSet)
|
||||
router.register("sources/ldap", LDAPSourceViewSet)
|
||||
router.register("sources/saml", SAMLSourceViewSet)
|
||||
router.register("sources/oauth", OAuthSourceViewSet)
|
||||
|
@ -1,46 +1,6 @@
|
||||
"""authentik Blueprints app"""
|
||||
|
||||
from importlib import import_module
|
||||
from inspect import ismethod
|
||||
|
||||
from django.apps import AppConfig
|
||||
from django.db import DatabaseError, InternalError, ProgrammingError
|
||||
from structlog.stdlib import BoundLogger, get_logger
|
||||
|
||||
|
||||
class ManagedAppConfig(AppConfig):
|
||||
"""Basic reconciliation logic for apps"""
|
||||
|
||||
_logger: BoundLogger
|
||||
|
||||
def __init__(self, app_name: str, *args, **kwargs) -> None:
|
||||
super().__init__(app_name, *args, **kwargs)
|
||||
self._logger = get_logger().bind(app_name=app_name)
|
||||
|
||||
def ready(self) -> None:
|
||||
self.reconcile()
|
||||
return super().ready()
|
||||
|
||||
def import_module(self, path: str):
|
||||
"""Load module"""
|
||||
import_module(path)
|
||||
|
||||
def reconcile(self) -> None:
|
||||
"""reconcile ourselves"""
|
||||
prefix = "reconcile_"
|
||||
for meth_name in dir(self):
|
||||
meth = getattr(self, meth_name)
|
||||
if not ismethod(meth):
|
||||
continue
|
||||
if not meth_name.startswith(prefix):
|
||||
continue
|
||||
name = meth_name.replace(prefix, "")
|
||||
try:
|
||||
self._logger.debug("Starting reconciler", name=name)
|
||||
meth()
|
||||
self._logger.debug("Successfully reconciled", name=name)
|
||||
except (DatabaseError, ProgrammingError, InternalError) as exc:
|
||||
self._logger.debug("Failed to run reconcile", name=name, exc=exc)
|
||||
from authentik.blueprints.manager import ManagedAppConfig
|
||||
|
||||
|
||||
class AuthentikBlueprintsConfig(ManagedAppConfig):
|
||||
@ -60,7 +20,3 @@ class AuthentikBlueprintsConfig(ManagedAppConfig):
|
||||
from authentik.blueprints.v1.tasks import blueprints_discover
|
||||
|
||||
blueprints_discover.delay()
|
||||
|
||||
def import_models(self):
|
||||
super().import_models()
|
||||
self.import_module("authentik.blueprints.v1.meta.apply_blueprint")
|
||||
|
@ -1,6 +1,4 @@
|
||||
"""Apply blueprint from commandline"""
|
||||
from sys import exit as sys_exit
|
||||
|
||||
from django.core.management.base import BaseCommand, no_translations
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
@ -22,9 +20,8 @@ class Command(BaseCommand):
|
||||
valid, logs = importer.validate()
|
||||
if not valid:
|
||||
for log in logs:
|
||||
getattr(LOGGER, log.pop("log_level"))(**log)
|
||||
self.stderr.write("blueprint invalid")
|
||||
sys_exit(1)
|
||||
LOGGER.debug(**log)
|
||||
raise ValueError("blueprint invalid")
|
||||
importer.apply()
|
||||
|
||||
def add_arguments(self, parser):
|
||||
|
@ -2,11 +2,12 @@
|
||||
from json import dumps, loads
|
||||
from pathlib import Path
|
||||
|
||||
from django.apps import apps
|
||||
from django.core.management.base import BaseCommand, no_translations
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.blueprints.v1.importer import is_model_allowed
|
||||
from authentik.blueprints.v1.meta.registry import registry
|
||||
from authentik.lib.models import SerializerModel
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
@ -28,9 +29,10 @@ class Command(BaseCommand):
|
||||
def set_model_allowed(self):
|
||||
"""Set model enum"""
|
||||
model_names = []
|
||||
for model in registry.get_models():
|
||||
for model in apps.get_models():
|
||||
if not is_model_allowed(model):
|
||||
continue
|
||||
if SerializerModel not in model.__mro__:
|
||||
continue
|
||||
model_names.append(f"{model._meta.app_label}.{model._meta.model_name}")
|
||||
model_names.sort()
|
||||
self.schema["properties"]["entries"]["items"]["properties"]["model"]["enum"] = model_names
|
||||
|
@ -41,7 +41,8 @@
|
||||
"$id": "#entry",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"model"
|
||||
"model",
|
||||
"identifiers"
|
||||
],
|
||||
"properties": {
|
||||
"model": {
|
||||
@ -66,7 +67,6 @@
|
||||
},
|
||||
"identifiers": {
|
||||
"type": "object",
|
||||
"default": {},
|
||||
"properties": {
|
||||
"pk": {
|
||||
"description": "Commonly available field, may not exist on all models",
|
||||
|
44
authentik/blueprints/manager.py
Normal file
44
authentik/blueprints/manager.py
Normal file
@ -0,0 +1,44 @@
|
||||
"""Managed objects manager"""
|
||||
from importlib import import_module
|
||||
from inspect import ismethod
|
||||
|
||||
from django.apps import AppConfig
|
||||
from django.db import DatabaseError, InternalError, ProgrammingError
|
||||
from structlog.stdlib import BoundLogger, get_logger
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
class ManagedAppConfig(AppConfig):
|
||||
"""Basic reconciliation logic for apps"""
|
||||
|
||||
_logger: BoundLogger
|
||||
|
||||
def __init__(self, app_name: str, *args, **kwargs) -> None:
|
||||
super().__init__(app_name, *args, **kwargs)
|
||||
self._logger = get_logger().bind(app_name=app_name)
|
||||
|
||||
def ready(self) -> None:
|
||||
self.reconcile()
|
||||
return super().ready()
|
||||
|
||||
def import_module(self, path: str):
|
||||
"""Load module"""
|
||||
import_module(path)
|
||||
|
||||
def reconcile(self) -> None:
|
||||
"""reconcile ourselves"""
|
||||
prefix = "reconcile_"
|
||||
for meth_name in dir(self):
|
||||
meth = getattr(self, meth_name)
|
||||
if not ismethod(meth):
|
||||
continue
|
||||
if not meth_name.startswith(prefix):
|
||||
continue
|
||||
name = meth_name.replace(prefix, "")
|
||||
try:
|
||||
self._logger.debug("Starting reconciler", name=name)
|
||||
meth()
|
||||
self._logger.debug("Successfully reconciled", name=name)
|
||||
except (DatabaseError, ProgrammingError, InternalError) as exc:
|
||||
self._logger.debug("Failed to run reconcile", name=name, exc=exc)
|
@ -4,7 +4,7 @@ from glob import glob
|
||||
from pathlib import Path
|
||||
|
||||
import django.contrib.postgres.fields
|
||||
from dacite.core import from_dict
|
||||
from dacite import from_dict
|
||||
from django.apps.registry import Apps
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
@ -1,4 +1,4 @@
|
||||
"""blueprint models"""
|
||||
"""Managed Object models"""
|
||||
from pathlib import Path
|
||||
from urllib.parse import urlparse
|
||||
from uuid import uuid4
|
||||
@ -6,26 +6,13 @@ from uuid import uuid4
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from opencontainers.distribution.reggie import (
|
||||
NewClient,
|
||||
WithDebug,
|
||||
WithDefaultName,
|
||||
WithDigest,
|
||||
WithReference,
|
||||
WithUserAgent,
|
||||
WithUsernamePassword,
|
||||
)
|
||||
from requests.exceptions import RequestException
|
||||
from requests import RequestException
|
||||
from rest_framework.serializers import Serializer
|
||||
from structlog import get_logger
|
||||
|
||||
from authentik.lib.config import CONFIG
|
||||
from authentik.lib.models import CreatedUpdatedModel, SerializerModel
|
||||
from authentik.lib.sentry import SentryIgnoredException
|
||||
from authentik.lib.utils.http import authentik_user_agent
|
||||
|
||||
OCI_MEDIA_TYPE = "application/vnd.goauthentik.blueprint.v1+yaml"
|
||||
LOGGER = get_logger()
|
||||
from authentik.lib.utils.http import get_http_session
|
||||
|
||||
|
||||
class BlueprintRetrievalFailed(SentryIgnoredException):
|
||||
@ -84,63 +71,18 @@ class BlueprintInstance(SerializerModel, ManagedModel, CreatedUpdatedModel):
|
||||
enabled = models.BooleanField(default=True)
|
||||
managed_models = ArrayField(models.TextField(), default=list)
|
||||
|
||||
def retrieve_oci(self) -> str:
|
||||
"""Get blueprint from an OCI registry"""
|
||||
url = urlparse(self.path)
|
||||
ref = "latest"
|
||||
path = url.path[1:]
|
||||
if ":" in url.path:
|
||||
path, _, ref = path.partition(":")
|
||||
client = NewClient(
|
||||
f"{url.scheme}://{url.hostname}",
|
||||
WithUserAgent(authentik_user_agent()),
|
||||
WithUsernamePassword(url.username, url.password),
|
||||
WithDefaultName(path),
|
||||
WithDebug(True),
|
||||
)
|
||||
LOGGER.info("Fetching OCI manifests for blueprint", instance=self)
|
||||
manifest_request = client.NewRequest(
|
||||
"GET",
|
||||
"/v2/<name>/manifests/<reference>",
|
||||
WithReference(ref),
|
||||
).SetHeader("Accept", "application/vnd.oci.image.manifest.v1+json")
|
||||
try:
|
||||
manifest_response = client.Do(manifest_request)
|
||||
manifest_response.raise_for_status()
|
||||
except RequestException as exc:
|
||||
raise BlueprintRetrievalFailed(exc) from exc
|
||||
manifest = manifest_response.json()
|
||||
if "errors" in manifest:
|
||||
raise BlueprintRetrievalFailed(manifest["errors"])
|
||||
|
||||
blob = None
|
||||
for layer in manifest.get("layers", []):
|
||||
if layer.get("mediaType", "") == OCI_MEDIA_TYPE:
|
||||
blob = layer.get("digest")
|
||||
LOGGER.debug("Found layer with matching media type", instance=self, blob=blob)
|
||||
if not blob:
|
||||
raise BlueprintRetrievalFailed("Blob not found")
|
||||
|
||||
blob_request = client.NewRequest(
|
||||
"GET",
|
||||
"/v2/<name>/blobs/<digest>",
|
||||
WithDigest(blob),
|
||||
)
|
||||
try:
|
||||
blob_response = client.Do(blob_request)
|
||||
blob_response.raise_for_status()
|
||||
return blob_response.text
|
||||
except RequestException as exc:
|
||||
raise BlueprintRetrievalFailed(exc) from exc
|
||||
|
||||
def retrieve(self) -> str:
|
||||
"""Retrieve blueprint contents"""
|
||||
full_path = Path(CONFIG.y("blueprints_dir")).joinpath(Path(self.path))
|
||||
if full_path.exists():
|
||||
LOGGER.debug("Blueprint path exists locally", instance=self)
|
||||
with full_path.open("r", encoding="utf-8") as _file:
|
||||
return _file.read()
|
||||
return self.retrieve_oci()
|
||||
if urlparse(self.path).scheme != "":
|
||||
try:
|
||||
res = get_http_session().get(self.path, timeout=3, allow_redirects=True)
|
||||
res.raise_for_status()
|
||||
return res.text
|
||||
except RequestException as exc:
|
||||
raise BlueprintRetrievalFailed(exc) from exc
|
||||
path = Path(CONFIG.y("blueprints_dir")).joinpath(Path(self.path))
|
||||
with path.open("r", encoding="utf-8") as _file:
|
||||
return _file.read()
|
||||
|
||||
@property
|
||||
def serializer(self) -> Serializer:
|
||||
|
@ -5,7 +5,7 @@ from typing import Callable
|
||||
|
||||
from django.apps import apps
|
||||
|
||||
from authentik.blueprints.apps import ManagedAppConfig
|
||||
from authentik.blueprints.manager import ManagedAppConfig
|
||||
from authentik.blueprints.models import BlueprintInstance
|
||||
from authentik.lib.config import CONFIG
|
||||
|
||||
|
@ -1,97 +0,0 @@
|
||||
"""Test blueprints OCI"""
|
||||
from django.test import TransactionTestCase
|
||||
from requests_mock import Mocker
|
||||
|
||||
from authentik.blueprints.models import OCI_MEDIA_TYPE, BlueprintInstance, BlueprintRetrievalFailed
|
||||
|
||||
|
||||
class TestBlueprintOCI(TransactionTestCase):
|
||||
"""Test Blueprints OCI Tasks"""
|
||||
|
||||
def test_successful(self):
|
||||
"""Successful retrieval"""
|
||||
with Mocker() as mocker:
|
||||
mocker.get(
|
||||
"https://ghcr.io/v2/goauthentik/blueprints/test/manifests/latest",
|
||||
json={
|
||||
"layers": [
|
||||
{
|
||||
"mediaType": OCI_MEDIA_TYPE,
|
||||
"digest": "foo",
|
||||
}
|
||||
]
|
||||
},
|
||||
)
|
||||
mocker.get("https://ghcr.io/v2/goauthentik/blueprints/test/blobs/foo", text="foo")
|
||||
|
||||
self.assertEqual(
|
||||
BlueprintInstance(
|
||||
path="https://ghcr.io/goauthentik/blueprints/test:latest"
|
||||
).retrieve_oci(),
|
||||
"foo",
|
||||
)
|
||||
|
||||
def test_manifests_error(self):
|
||||
"""Test manifests request erroring"""
|
||||
with Mocker() as mocker:
|
||||
mocker.get(
|
||||
"https://ghcr.io/v2/goauthentik/blueprints/test/manifests/latest", status_code=401
|
||||
)
|
||||
|
||||
with self.assertRaises(BlueprintRetrievalFailed):
|
||||
BlueprintInstance(
|
||||
path="https://ghcr.io/goauthentik/blueprints/test:latest"
|
||||
).retrieve_oci()
|
||||
|
||||
def test_manifests_error_response(self):
|
||||
"""Test manifests request erroring"""
|
||||
with Mocker() as mocker:
|
||||
mocker.get(
|
||||
"https://ghcr.io/v2/goauthentik/blueprints/test/manifests/latest",
|
||||
json={"errors": ["foo"]},
|
||||
)
|
||||
|
||||
with self.assertRaises(BlueprintRetrievalFailed):
|
||||
BlueprintInstance(
|
||||
path="https://ghcr.io/goauthentik/blueprints/test:latest"
|
||||
).retrieve_oci()
|
||||
|
||||
def test_no_matching_blob(self):
|
||||
"""Successful retrieval"""
|
||||
with Mocker() as mocker:
|
||||
mocker.get(
|
||||
"https://ghcr.io/v2/goauthentik/blueprints/test/manifests/latest",
|
||||
json={
|
||||
"layers": [
|
||||
{
|
||||
"mediaType": OCI_MEDIA_TYPE + "foo",
|
||||
"digest": "foo",
|
||||
}
|
||||
]
|
||||
},
|
||||
)
|
||||
with self.assertRaises(BlueprintRetrievalFailed):
|
||||
BlueprintInstance(
|
||||
path="https://ghcr.io/goauthentik/blueprints/test:latest"
|
||||
).retrieve_oci()
|
||||
|
||||
def test_blob_error(self):
|
||||
"""Successful retrieval"""
|
||||
with Mocker() as mocker:
|
||||
mocker.get(
|
||||
"https://ghcr.io/v2/goauthentik/blueprints/test/manifests/latest",
|
||||
json={
|
||||
"layers": [
|
||||
{
|
||||
"mediaType": OCI_MEDIA_TYPE,
|
||||
"digest": "foo",
|
||||
}
|
||||
]
|
||||
},
|
||||
)
|
||||
mocker.get("https://ghcr.io/v2/goauthentik/blueprints/test/blobs/foo", status_code=401)
|
||||
|
||||
with self.assertRaises(BlueprintRetrievalFailed):
|
||||
BlueprintInstance(
|
||||
path="https://ghcr.io/goauthentik/blueprints/test:latest"
|
||||
).retrieve_oci()
|
@ -33,6 +33,4 @@ def blueprint_tester(file_name: Path) -> Callable:
|
||||
|
||||
|
||||
for blueprint_file in Path("blueprints/").glob("**/*.yaml"):
|
||||
if "local" in str(blueprint_file):
|
||||
continue
|
||||
setattr(TestPackaged, f"test_blueprint_{blueprint_file}", blueprint_tester(blueprint_file))
|
||||
|
@ -45,8 +45,8 @@ class BlueprintEntryState:
|
||||
class BlueprintEntry:
|
||||
"""Single entry of a blueprint"""
|
||||
|
||||
identifiers: dict[str, Any]
|
||||
model: str
|
||||
identifiers: dict[str, Any] = field(default_factory=dict)
|
||||
attrs: Optional[dict[str, Any]] = field(default_factory=dict)
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
@ -63,7 +63,7 @@ class BlueprintEntry:
|
||||
all_attrs = get_attrs(model)
|
||||
|
||||
for extra_identifier_name in extra_identifier_names:
|
||||
identifiers[extra_identifier_name] = all_attrs.pop(extra_identifier_name, None)
|
||||
identifiers[extra_identifier_name] = all_attrs.pop(extra_identifier_name)
|
||||
return BlueprintEntry(
|
||||
identifiers=identifiers,
|
||||
model=f"{model._meta.app_label}.{model._meta.model_name}",
|
||||
@ -105,9 +105,9 @@ class Blueprint:
|
||||
|
||||
version: int = field(default=1)
|
||||
entries: list[BlueprintEntry] = field(default_factory=list)
|
||||
context: dict = field(default_factory=dict)
|
||||
|
||||
metadata: Optional[BlueprintMetadata] = field(default=None)
|
||||
context: Optional[dict] = field(default_factory=dict)
|
||||
|
||||
|
||||
class YAMLTag:
|
||||
@ -139,7 +139,7 @@ class KeyOf(YAMLTag):
|
||||
):
|
||||
return _entry._state.instance.pbm_uuid
|
||||
return _entry._state.instance.pk
|
||||
raise EntryInvalidError(
|
||||
raise ValueError(
|
||||
f"KeyOf: failed to find entry with `id` of `{self.id_from}` and a model instance"
|
||||
)
|
||||
|
||||
@ -227,7 +227,6 @@ class BlueprintDumper(SafeDumper):
|
||||
self.add_representer(UUID, lambda self, data: self.represent_str(str(data)))
|
||||
self.add_representer(OrderedDict, lambda self, data: self.represent_dict(dict(data)))
|
||||
self.add_representer(Enum, lambda self, data: self.represent_str(data.value))
|
||||
self.add_representer(None, lambda self, data: self.represent_str(str(data)))
|
||||
|
||||
def represent(self, data) -> None:
|
||||
if is_dataclass(data):
|
||||
@ -254,9 +253,3 @@ class BlueprintLoader(SafeLoader):
|
||||
|
||||
class EntryInvalidError(SentryIgnoredException):
|
||||
"""Error raised when an entry is invalid"""
|
||||
|
||||
serializer_errors: Optional[dict]
|
||||
|
||||
def __init__(self, *args: object, serializer_errors: Optional[dict] = None) -> None:
|
||||
super().__init__(*args)
|
||||
self.serializer_errors = serializer_errors
|
||||
|
@ -1,13 +1,11 @@
|
||||
"""Blueprint exporter"""
|
||||
from typing import Iterable
|
||||
from typing import Iterator
|
||||
from uuid import UUID
|
||||
|
||||
from django.apps import apps
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.db.models import Model, Q, QuerySet
|
||||
from django.db.models import Q
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext as _
|
||||
from guardian.shortcuts import get_anonymous_user
|
||||
from yaml import dump
|
||||
|
||||
from authentik.blueprints.v1.common import (
|
||||
@ -18,8 +16,8 @@ from authentik.blueprints.v1.common import (
|
||||
)
|
||||
from authentik.blueprints.v1.importer import is_model_allowed
|
||||
from authentik.blueprints.v1.labels import LABEL_AUTHENTIK_GENERATED
|
||||
from authentik.events.models import Event
|
||||
from authentik.flows.models import Flow, FlowStageBinding, Stage
|
||||
from authentik.lib.models import SerializerModel
|
||||
from authentik.policies.models import Policy, PolicyBinding
|
||||
from authentik.stages.prompt.models import PromptStage
|
||||
|
||||
@ -27,30 +25,23 @@ from authentik.stages.prompt.models import PromptStage
|
||||
class Exporter:
|
||||
"""Export flow with attached stages into yaml"""
|
||||
|
||||
excluded_models: list[type[Model]] = []
|
||||
excluded_models = []
|
||||
|
||||
def __init__(self):
|
||||
self.excluded_models = [
|
||||
Event,
|
||||
]
|
||||
self.excluded_models = []
|
||||
|
||||
def get_entries(self) -> Iterable[BlueprintEntry]:
|
||||
def get_entries(self) -> Iterator[BlueprintEntry]:
|
||||
"""Get blueprint entries"""
|
||||
for model in apps.get_models():
|
||||
if not is_model_allowed(model):
|
||||
continue
|
||||
if model in self.excluded_models:
|
||||
continue
|
||||
for obj in self.get_model_instances(model):
|
||||
if SerializerModel not in model.__mro__:
|
||||
continue
|
||||
for obj in model.objects.all():
|
||||
yield BlueprintEntry.from_model(obj)
|
||||
|
||||
def get_model_instances(self, model: type[Model]) -> QuerySet:
|
||||
"""Return a queryset for `model`. Can be used to filter some
|
||||
objects on some models"""
|
||||
if model == get_user_model():
|
||||
return model.objects.exclude(pk=get_anonymous_user().pk)
|
||||
return model.objects.all()
|
||||
|
||||
def _pre_export(self, blueprint: Blueprint):
|
||||
"""Hook to run anything pre-export"""
|
||||
|
||||
@ -96,7 +87,7 @@ class FlowExporter(Exporter):
|
||||
"pbm_uuid", flat=True
|
||||
)
|
||||
|
||||
def walk_stages(self) -> Iterable[BlueprintEntry]:
|
||||
def walk_stages(self) -> Iterator[BlueprintEntry]:
|
||||
"""Convert all stages attached to self.flow into BlueprintEntry objects"""
|
||||
stages = Stage.objects.filter(flow=self.flow).select_related().select_subclasses()
|
||||
for stage in stages:
|
||||
@ -104,13 +95,13 @@ class FlowExporter(Exporter):
|
||||
pass
|
||||
yield BlueprintEntry.from_model(stage, "name")
|
||||
|
||||
def walk_stage_bindings(self) -> Iterable[BlueprintEntry]:
|
||||
def walk_stage_bindings(self) -> Iterator[BlueprintEntry]:
|
||||
"""Convert all bindings attached to self.flow into BlueprintEntry objects"""
|
||||
bindings = FlowStageBinding.objects.filter(target=self.flow).select_related()
|
||||
for binding in bindings:
|
||||
yield BlueprintEntry.from_model(binding, "target", "stage", "order")
|
||||
|
||||
def walk_policies(self) -> Iterable[BlueprintEntry]:
|
||||
def walk_policies(self) -> Iterator[BlueprintEntry]:
|
||||
"""Walk over all policies. This is done at the beginning of the export for stages that have
|
||||
a direct foreign key to a policy."""
|
||||
# Special case for PromptStage as that has a direct M2M to policy, we have to ensure
|
||||
@ -121,21 +112,21 @@ class FlowExporter(Exporter):
|
||||
for policy in policies:
|
||||
yield BlueprintEntry.from_model(policy)
|
||||
|
||||
def walk_policy_bindings(self) -> Iterable[BlueprintEntry]:
|
||||
def walk_policy_bindings(self) -> Iterator[BlueprintEntry]:
|
||||
"""Walk over all policybindings relative to us. This is run at the end of the export, as
|
||||
we are sure all objects exist now."""
|
||||
bindings = PolicyBinding.objects.filter(target__in=self.pbm_uuids).select_related()
|
||||
for binding in bindings:
|
||||
yield BlueprintEntry.from_model(binding, "policy", "target", "order")
|
||||
|
||||
def walk_stage_prompts(self) -> Iterable[BlueprintEntry]:
|
||||
def walk_stage_prompts(self) -> Iterator[BlueprintEntry]:
|
||||
"""Walk over all prompts associated with any PromptStages"""
|
||||
prompt_stages = PromptStage.objects.filter(flow=self.flow)
|
||||
for stage in prompt_stages:
|
||||
for prompt in stage.fields.all():
|
||||
yield BlueprintEntry.from_model(prompt)
|
||||
|
||||
def get_entries(self) -> Iterable[BlueprintEntry]:
|
||||
def get_entries(self) -> Iterator[BlueprintEntry]:
|
||||
entries = []
|
||||
entries.append(BlueprintEntry.from_model(self.flow, "slug"))
|
||||
if self.with_stage_prompts:
|
||||
|
@ -3,9 +3,10 @@ from contextlib import contextmanager
|
||||
from copy import deepcopy
|
||||
from typing import Any, Optional
|
||||
|
||||
from dacite.core import from_dict
|
||||
from dacite import from_dict
|
||||
from dacite.exceptions import DaciteError
|
||||
from deepmerge import always_merger
|
||||
from django.apps import apps
|
||||
from django.db import transaction
|
||||
from django.db.models import Model
|
||||
from django.db.models.query_utils import Q
|
||||
@ -24,7 +25,6 @@ from authentik.blueprints.v1.common import (
|
||||
BlueprintLoader,
|
||||
EntryInvalidError,
|
||||
)
|
||||
from authentik.blueprints.v1.meta.registry import BaseMetaModel, registry
|
||||
from authentik.core.models import (
|
||||
AuthenticatedSession,
|
||||
PropertyMapping,
|
||||
@ -59,7 +59,7 @@ def is_model_allowed(model: type[Model]) -> bool:
|
||||
# Classes that have other dependencies
|
||||
AuthenticatedSession,
|
||||
)
|
||||
return model not in excluded_models and issubclass(model, (SerializerModel, BaseMetaModel))
|
||||
return model not in excluded_models
|
||||
|
||||
|
||||
@contextmanager
|
||||
@ -138,20 +138,10 @@ class Importer:
|
||||
def _validate_single(self, entry: BlueprintEntry) -> BaseSerializer:
|
||||
"""Validate a single entry"""
|
||||
model_app_label, model_name = entry.model.split(".")
|
||||
model: type[SerializerModel] = registry.get_model(model_app_label, model_name)
|
||||
model: type[SerializerModel] = apps.get_model(model_app_label, model_name)
|
||||
# Don't use isinstance since we don't want to check for inheritance
|
||||
if not is_model_allowed(model):
|
||||
raise EntryInvalidError(f"Model {model} not allowed")
|
||||
if issubclass(model, BaseMetaModel):
|
||||
serializer_class: type[Serializer] = model.serializer()
|
||||
serializer = serializer_class(data=entry.get_attrs(self.__import))
|
||||
try:
|
||||
serializer.is_valid(raise_exception=True)
|
||||
except ValidationError as exc:
|
||||
raise EntryInvalidError(
|
||||
f"Serializer errors {serializer.errors}", serializer_errors=serializer.errors
|
||||
) from exc
|
||||
return serializer
|
||||
if entry.identifiers == {}:
|
||||
raise EntryInvalidError("No identifiers")
|
||||
|
||||
@ -168,7 +158,7 @@ class Importer:
|
||||
existing_models = model.objects.filter(self.__query_from_identifier(updated_identifiers))
|
||||
|
||||
serializer_kwargs = {}
|
||||
if not isinstance(model(), BaseMetaModel) and existing_models.exists():
|
||||
if existing_models.exists():
|
||||
model_instance = existing_models.first()
|
||||
self.logger.debug(
|
||||
"initialise serializer with instance",
|
||||
@ -179,18 +169,13 @@ class Importer:
|
||||
serializer_kwargs["instance"] = model_instance
|
||||
serializer_kwargs["partial"] = True
|
||||
else:
|
||||
self.logger.debug(
|
||||
"initialised new serializer instance", model=model, **updated_identifiers
|
||||
)
|
||||
self.logger.debug("initialise new instance", model=model, **updated_identifiers)
|
||||
model_instance = model()
|
||||
# pk needs to be set on the model instance otherwise a new one will be generated
|
||||
if "pk" in updated_identifiers:
|
||||
model_instance.pk = updated_identifiers["pk"]
|
||||
serializer_kwargs["instance"] = model_instance
|
||||
try:
|
||||
full_data = self.__update_pks_for_attrs(entry.get_attrs(self.__import))
|
||||
except ValueError as exc:
|
||||
raise EntryInvalidError(exc) from exc
|
||||
full_data = self.__update_pks_for_attrs(entry.get_attrs(self.__import))
|
||||
full_data.update(updated_identifiers)
|
||||
serializer_kwargs["data"] = full_data
|
||||
|
||||
@ -198,9 +183,7 @@ class Importer:
|
||||
try:
|
||||
serializer.is_valid(raise_exception=True)
|
||||
except ValidationError as exc:
|
||||
raise EntryInvalidError(
|
||||
f"Serializer errors {serializer.errors}", serializer_errors=serializer.errors
|
||||
) from exc
|
||||
raise EntryInvalidError(f"Serializer errors {serializer.errors}") from exc
|
||||
return serializer
|
||||
|
||||
def apply(self) -> bool:
|
||||
@ -222,7 +205,7 @@ class Importer:
|
||||
for entry in self.__import.entries:
|
||||
model_app_label, model_name = entry.model.split(".")
|
||||
try:
|
||||
model: type[SerializerModel] = registry.get_model(model_app_label, model_name)
|
||||
model: SerializerModel = apps.get_model(model_app_label, model_name)
|
||||
except LookupError:
|
||||
self.logger.warning(
|
||||
"app or model does not exist", app=model_app_label, model=model_name
|
||||
@ -232,14 +215,14 @@ class Importer:
|
||||
try:
|
||||
serializer = self._validate_single(entry)
|
||||
except EntryInvalidError as exc:
|
||||
self.logger.warning(f"entry invalid: {exc}", entry=entry, error=exc)
|
||||
self.logger.warning("entry invalid", entry=entry, error=exc)
|
||||
return False
|
||||
|
||||
model = serializer.save()
|
||||
if "pk" in entry.identifiers:
|
||||
self.__pk_map[entry.identifiers["pk"]] = model.pk
|
||||
entry._state = BlueprintEntryState(model)
|
||||
self.logger.debug("updated model", model=model)
|
||||
self.logger.debug("updated model", model=model, pk=model.pk)
|
||||
return True
|
||||
|
||||
def validate(self) -> tuple[bool, list[EventDict]]:
|
||||
@ -256,8 +239,8 @@ class Importer:
|
||||
):
|
||||
successful = self._apply_models()
|
||||
if not successful:
|
||||
self.logger.debug("Blueprint validation failed")
|
||||
self.logger.debug("blueprint validation failed")
|
||||
for log in logs:
|
||||
getattr(self.logger, log.get("log_level"))(**log)
|
||||
self.logger.debug(**log)
|
||||
self.__import = orig_import
|
||||
return successful, logs
|
||||
|
@ -1,60 +0,0 @@
|
||||
"""Apply Blueprint meta model"""
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.fields import BooleanField, JSONField
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.blueprints.v1.meta.registry import BaseMetaModel, MetaResult, registry
|
||||
from authentik.core.api.utils import PassiveSerializer, is_dict
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from authentik.blueprints.models import BlueprintInstance
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
class ApplyBlueprintMetaSerializer(PassiveSerializer):
|
||||
"""Serializer for meta apply blueprint model"""
|
||||
|
||||
identifiers = JSONField(validators=[is_dict])
|
||||
required = BooleanField(default=True)
|
||||
|
||||
# We cannot override `instance` as that will confuse rest_framework
|
||||
# and make it attempt to update the instance
|
||||
blueprint_instance: "BlueprintInstance"
|
||||
|
||||
def validate(self, attrs):
|
||||
from authentik.blueprints.models import BlueprintInstance
|
||||
|
||||
identifiers = attrs["identifiers"]
|
||||
required = attrs["required"]
|
||||
instance = BlueprintInstance.objects.filter(**identifiers).first()
|
||||
if not instance and required:
|
||||
raise ValidationError("Required blueprint does not exist")
|
||||
self.blueprint_instance = instance
|
||||
return super().validate(attrs)
|
||||
|
||||
def create(self, validated_data: dict) -> MetaResult:
|
||||
from authentik.blueprints.v1.tasks import apply_blueprint
|
||||
|
||||
if not self.blueprint_instance:
|
||||
LOGGER.info("Blueprint does not exist, but not required")
|
||||
return MetaResult()
|
||||
LOGGER.debug("Applying blueprint from meta model", blueprint=self.blueprint_instance)
|
||||
# pylint: disable=no-value-for-parameter
|
||||
apply_blueprint(str(self.blueprint_instance.pk))
|
||||
return MetaResult()
|
||||
|
||||
|
||||
@registry.register("metaapplyblueprint")
|
||||
class MetaApplyBlueprint(BaseMetaModel):
|
||||
"""Meta model to apply another blueprint"""
|
||||
|
||||
@staticmethod
|
||||
def serializer() -> ApplyBlueprintMetaSerializer:
|
||||
return ApplyBlueprintMetaSerializer
|
||||
|
||||
class Meta:
|
||||
|
||||
abstract = True
|
@ -1,61 +0,0 @@
|
||||
"""Base models"""
|
||||
from django.apps import apps
|
||||
from django.db.models import Model
|
||||
from rest_framework.serializers import Serializer
|
||||
|
||||
|
||||
class BaseMetaModel(Model):
|
||||
"""Base models"""
|
||||
|
||||
@staticmethod
|
||||
def serializer() -> Serializer:
|
||||
"""Serializer similar to SerializerModel, but as a static method since
|
||||
this is an abstract model"""
|
||||
raise NotImplementedError
|
||||
|
||||
class Meta:
|
||||
|
||||
abstract = True
|
||||
|
||||
|
||||
class MetaResult:
|
||||
"""Result returned by Meta Models' serializers. Empty class but we can't return none as
|
||||
the framework doesn't allow that"""
|
||||
|
||||
|
||||
class MetaModelRegistry:
|
||||
"""Registry for pseudo meta models"""
|
||||
|
||||
models: dict[str, BaseMetaModel]
|
||||
virtual_prefix: str
|
||||
|
||||
def __init__(self, prefix: str) -> None:
|
||||
self.models = {}
|
||||
self.virtual_prefix = prefix
|
||||
|
||||
def register(self, model_id: str):
|
||||
"""Register model class under `model_id`"""
|
||||
|
||||
def inner_wrapper(cls):
|
||||
self.models[model_id] = cls
|
||||
return cls
|
||||
|
||||
return inner_wrapper
|
||||
|
||||
def get_models(self):
|
||||
"""Wrapper for django's `get_models` to list all models"""
|
||||
models = apps.get_models()
|
||||
for _, value in self.models.items():
|
||||
models.append(value)
|
||||
return models
|
||||
|
||||
def get_model(self, app_label: str, model_id: str) -> type[Model]:
|
||||
"""Get model checks if any virtual models are registered, and falls back
|
||||
to actual django models"""
|
||||
if app_label.lower() == self.virtual_prefix:
|
||||
if model_id.lower() in self.models:
|
||||
return self.models[model_id]
|
||||
return apps.get_model(app_label, model_id)
|
||||
|
||||
|
||||
registry = MetaModelRegistry("authentik_blueprints")
|
@ -4,9 +4,8 @@ from hashlib import sha512
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from dacite.core import from_dict
|
||||
from dacite import from_dict
|
||||
from django.db import DatabaseError, InternalError, ProgrammingError
|
||||
from django.utils.text import slugify
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from structlog.stdlib import get_logger
|
||||
@ -18,7 +17,7 @@ from authentik.blueprints.models import (
|
||||
BlueprintInstanceStatus,
|
||||
BlueprintRetrievalFailed,
|
||||
)
|
||||
from authentik.blueprints.v1.common import BlueprintLoader, BlueprintMetadata, EntryInvalidError
|
||||
from authentik.blueprints.v1.common import BlueprintLoader, BlueprintMetadata
|
||||
from authentik.blueprints.v1.importer import Importer
|
||||
from authentik.blueprints.v1.labels import LABEL_AUTHENTIK_INSTANTIATE
|
||||
from authentik.events.monitored_tasks import (
|
||||
@ -77,9 +76,7 @@ def blueprints_find():
|
||||
LOGGER.warning("invalid blueprint version", version=version, path=str(path))
|
||||
continue
|
||||
file_hash = sha512(path.read_bytes()).hexdigest()
|
||||
blueprint = BlueprintFile(
|
||||
str(path.relative_to(root)), version, file_hash, int(path.stat().st_mtime)
|
||||
)
|
||||
blueprint = BlueprintFile(path.relative_to(root), version, file_hash, path.stat().st_mtime)
|
||||
blueprint.meta = from_dict(BlueprintMetadata, metadata) if metadata else None
|
||||
blueprints.append(blueprint)
|
||||
LOGGER.info(
|
||||
@ -128,7 +125,9 @@ def check_blueprint_v1_file(blueprint: BlueprintFile):
|
||||
)
|
||||
instance.save()
|
||||
if instance.last_applied_hash != blueprint.hash:
|
||||
apply_blueprint.delay(str(instance.pk))
|
||||
instance.metadata = asdict(blueprint.meta) if blueprint.meta else {}
|
||||
instance.save()
|
||||
apply_blueprint.delay(instance.pk.hex)
|
||||
|
||||
|
||||
@CELERY_APP.task(
|
||||
@ -137,18 +136,15 @@ def check_blueprint_v1_file(blueprint: BlueprintFile):
|
||||
)
|
||||
def apply_blueprint(self: MonitoredTask, instance_pk: str):
|
||||
"""Apply single blueprint"""
|
||||
self.set_uid(instance_pk)
|
||||
self.save_on_success = False
|
||||
instance: Optional[BlueprintInstance] = None
|
||||
try:
|
||||
instance: BlueprintInstance = BlueprintInstance.objects.filter(pk=instance_pk).first()
|
||||
self.set_uid(slugify(instance.name))
|
||||
if not instance or not instance.enabled:
|
||||
return
|
||||
blueprint_content = instance.retrieve()
|
||||
file_hash = sha512(blueprint_content.encode()).hexdigest()
|
||||
importer = Importer(blueprint_content, instance.context)
|
||||
if importer.blueprint.metadata:
|
||||
instance.metadata = asdict(importer.blueprint.metadata)
|
||||
valid, logs = importer.validate()
|
||||
if not valid:
|
||||
instance.status = BlueprintInstanceStatus.ERROR
|
||||
@ -164,6 +160,7 @@ def apply_blueprint(self: MonitoredTask, instance_pk: str):
|
||||
instance.status = BlueprintInstanceStatus.SUCCESSFUL
|
||||
instance.last_applied_hash = file_hash
|
||||
instance.last_applied = now()
|
||||
instance.save()
|
||||
self.set_status(TaskResult(TaskResultStatus.SUCCESSFUL))
|
||||
except (
|
||||
DatabaseError,
|
||||
@ -171,11 +168,7 @@ def apply_blueprint(self: MonitoredTask, instance_pk: str):
|
||||
InternalError,
|
||||
IOError,
|
||||
BlueprintRetrievalFailed,
|
||||
EntryInvalidError,
|
||||
) as exc:
|
||||
if instance:
|
||||
instance.status = BlueprintInstanceStatus.ERROR
|
||||
instance.status = BlueprintInstanceStatus.ERROR
|
||||
instance.save()
|
||||
self.set_status(TaskResult(TaskResultStatus.ERROR).with_error(exc))
|
||||
finally:
|
||||
if instance:
|
||||
instance.save()
|
||||
|
@ -50,9 +50,7 @@ class ApplicationSerializer(ModelSerializer):
|
||||
|
||||
def get_launch_url(self, app: Application) -> Optional[str]:
|
||||
"""Allow formatting of launch URL"""
|
||||
user = None
|
||||
if "request" in self.context:
|
||||
user = self.context["request"].user
|
||||
user = self.context["request"].user
|
||||
return app.get_launch_url(user)
|
||||
|
||||
class Meta:
|
||||
@ -232,11 +230,7 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet):
|
||||
return Response({})
|
||||
if icon:
|
||||
app.meta_icon = icon
|
||||
try:
|
||||
app.save()
|
||||
except PermissionError as exc:
|
||||
LOGGER.warning("Failed to save icon", exc=exc)
|
||||
return HttpResponseBadRequest()
|
||||
app.save()
|
||||
return Response({})
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
|
@ -17,7 +17,7 @@ from authentik.api.decorators import permission_required
|
||||
from authentik.blueprints.api import ManagedSerializer
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.core.api.utils import MetaNameSerializer, PassiveSerializer, TypeCreateSerializer
|
||||
from authentik.core.expression.evaluator import PropertyMappingEvaluator
|
||||
from authentik.core.expression import PropertyMappingEvaluator
|
||||
from authentik.core.models import PropertyMapping
|
||||
from authentik.lib.utils.reflection import all_subclasses
|
||||
from authentik.policies.api.exec import PolicyTestSerializer
|
||||
@ -41,9 +41,7 @@ class PropertyMappingSerializer(ManagedSerializer, ModelSerializer, MetaNameSeri
|
||||
|
||||
def validate_expression(self, expression: str) -> str:
|
||||
"""Test Syntax"""
|
||||
evaluator = PropertyMappingEvaluator(
|
||||
self.instance,
|
||||
)
|
||||
evaluator = PropertyMappingEvaluator()
|
||||
evaluator.validate(expression)
|
||||
return expression
|
||||
|
||||
|
@ -470,7 +470,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
||||
# pylint: disable=invalid-name, unused-argument
|
||||
def recovery_email(self, request: Request, pk: int) -> Response:
|
||||
"""Create a temporary link that a user can use to recover their accounts"""
|
||||
for_user: User = self.get_object()
|
||||
for_user = self.get_object()
|
||||
if for_user.email == "":
|
||||
LOGGER.debug("User doesn't have an email address")
|
||||
return Response(status=404)
|
||||
@ -488,9 +488,8 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
||||
email_stage: EmailStage = stages.first()
|
||||
message = TemplateEmailMessage(
|
||||
subject=_(email_stage.subject),
|
||||
to=[for_user.email],
|
||||
template_name=email_stage.template,
|
||||
language=for_user.locale(request),
|
||||
to=[for_user.email],
|
||||
template_context={
|
||||
"url": link,
|
||||
"user": for_user,
|
||||
|
@ -1,7 +1,7 @@
|
||||
"""authentik core app config"""
|
||||
from django.conf import settings
|
||||
|
||||
from authentik.blueprints.apps import ManagedAppConfig
|
||||
from authentik.blueprints.manager import ManagedAppConfig
|
||||
|
||||
|
||||
class AuthentikCoreConfig(ManagedAppConfig):
|
||||
|
@ -2,32 +2,28 @@
|
||||
from traceback import format_tb
|
||||
from typing import Optional
|
||||
|
||||
from django.db.models import Model
|
||||
from django.http import HttpRequest
|
||||
from guardian.utils import get_anonymous_user
|
||||
|
||||
from authentik.core.models import User
|
||||
from authentik.core.models import PropertyMapping, User
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.lib.expression.evaluator import BaseEvaluator
|
||||
from authentik.policies.types import PolicyRequest
|
||||
|
||||
|
||||
class PropertyMappingEvaluator(BaseEvaluator):
|
||||
"""Custom Evaluator that adds some different context variables."""
|
||||
"""Custom Evalautor that adds some different context variables."""
|
||||
|
||||
def __init__(
|
||||
def set_context(
|
||||
self,
|
||||
model: Model,
|
||||
user: Optional[User] = None,
|
||||
request: Optional[HttpRequest] = None,
|
||||
user: Optional[User],
|
||||
request: Optional[HttpRequest],
|
||||
mapping: PropertyMapping,
|
||||
**kwargs,
|
||||
):
|
||||
if hasattr(model, "name"):
|
||||
_filename = model.name
|
||||
else:
|
||||
_filename = str(model)
|
||||
super().__init__(filename=_filename)
|
||||
req = PolicyRequest(user=User())
|
||||
req.obj = model
|
||||
"""Update context with context from PropertyMapping's evaluate"""
|
||||
req = PolicyRequest(user=get_anonymous_user())
|
||||
req.obj = mapping
|
||||
if user:
|
||||
req.user = user
|
||||
self._context["user"] = user
|
@ -7,9 +7,9 @@ from django.core.management.base import BaseCommand
|
||||
from django.db.models import Model
|
||||
from django.db.models.signals import post_save, pre_delete
|
||||
|
||||
from authentik import get_full_version
|
||||
from authentik import __version__
|
||||
from authentik.core.models import User
|
||||
from authentik.events.middleware import should_log_model
|
||||
from authentik.events.middleware import IGNORED_MODELS
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.events.utils import model_to_dict
|
||||
|
||||
@ -18,7 +18,7 @@ BANNER_TEXT = """### authentik shell ({authentik})
|
||||
node=platform.node(),
|
||||
python=platform.python_version(),
|
||||
arch=platform.machine(),
|
||||
authentik=get_full_version(),
|
||||
authentik=__version__,
|
||||
)
|
||||
|
||||
|
||||
@ -50,7 +50,7 @@ class Command(BaseCommand):
|
||||
# pylint: disable=unused-argument
|
||||
def post_save_handler(sender, instance: Model, created: bool, **_):
|
||||
"""Signal handler for all object's post_save"""
|
||||
if not should_log_model(instance):
|
||||
if isinstance(instance, IGNORED_MODELS):
|
||||
return
|
||||
|
||||
action = EventAction.MODEL_CREATED if created else EventAction.MODEL_UPDATED
|
||||
@ -66,7 +66,7 @@ class Command(BaseCommand):
|
||||
# pylint: disable=unused-argument
|
||||
def pre_delete_handler(sender, instance: Model, **_):
|
||||
"""Signal handler for all object's pre_delete"""
|
||||
if not should_log_model(instance): # pragma: no cover
|
||||
if isinstance(instance, IGNORED_MODELS): # pragma: no cover
|
||||
return
|
||||
|
||||
Event.new(EventAction.MODEL_DELETED, model=model_to_dict(instance)).set_user(
|
||||
|
@ -1,10 +1,9 @@
|
||||
"""authentik admin Middleware to impersonate users"""
|
||||
from contextvars import ContextVar
|
||||
from typing import Callable, Optional
|
||||
from typing import Callable
|
||||
from uuid import uuid4
|
||||
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.utils.translation import activate
|
||||
from sentry_sdk.api import set_tag
|
||||
from structlog.contextvars import STRUCTLOG_KEY_PREFIX
|
||||
|
||||
@ -14,9 +13,9 @@ RESPONSE_HEADER_ID = "X-authentik-id"
|
||||
KEY_AUTH_VIA = "auth_via"
|
||||
KEY_USER = "user"
|
||||
|
||||
CTX_REQUEST_ID = ContextVar[Optional[str]](STRUCTLOG_KEY_PREFIX + "request_id", default=None)
|
||||
CTX_HOST = ContextVar[Optional[str]](STRUCTLOG_KEY_PREFIX + "host", default=None)
|
||||
CTX_AUTH_VIA = ContextVar[Optional[str]](STRUCTLOG_KEY_PREFIX + KEY_AUTH_VIA, default=None)
|
||||
CTX_REQUEST_ID = ContextVar(STRUCTLOG_KEY_PREFIX + "request_id", default=None)
|
||||
CTX_HOST = ContextVar(STRUCTLOG_KEY_PREFIX + "host", default=None)
|
||||
CTX_AUTH_VIA = ContextVar(STRUCTLOG_KEY_PREFIX + KEY_AUTH_VIA, default=None)
|
||||
|
||||
|
||||
class ImpersonateMiddleware:
|
||||
@ -30,10 +29,6 @@ class ImpersonateMiddleware:
|
||||
def __call__(self, request: HttpRequest) -> HttpResponse:
|
||||
# No permission checks are done here, they need to be checked before
|
||||
# SESSION_KEY_IMPERSONATE_USER is set.
|
||||
if request.user.is_authenticated:
|
||||
locale = request.user.locale(request)
|
||||
if locale != "":
|
||||
activate(locale)
|
||||
|
||||
if SESSION_KEY_IMPERSONATE_USER in request.session:
|
||||
request.user = request.session[SESSION_KEY_IMPERSONATE_USER]
|
||||
|
55
authentik/core/migrations/0002_auto_20200523_1133.py
Normal file
55
authentik/core/migrations/0002_auto_20200523_1133.py
Normal file
@ -0,0 +1,55 @@
|
||||
# Generated by Django 3.0.6 on 2020-05-23 11:33
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_flows", "0003_auto_20200523_1133"),
|
||||
("authentik_core", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name="application",
|
||||
name="skip_authorization",
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="source",
|
||||
name="authentication_flow",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
default=None,
|
||||
help_text="Flow to use when authenticating existing users.",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="source_authentication",
|
||||
to="authentik_flows.Flow",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="source",
|
||||
name="enrollment_flow",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
default=None,
|
||||
help_text="Flow to use when enrolling new users.",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="source_enrollment",
|
||||
to="authentik_flows.Flow",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="provider",
|
||||
name="authorization_flow",
|
||||
field=models.ForeignKey(
|
||||
help_text="Flow used when authorizing this provider.",
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="provider_authorization",
|
||||
to="authentik_flows.Flow",
|
||||
),
|
||||
),
|
||||
]
|
57
authentik/core/migrations/0003_default_user.py
Normal file
57
authentik/core/migrations/0003_default_user.py
Normal file
@ -0,0 +1,57 @@
|
||||
# Generated by Django 3.0.6 on 2020-05-23 16:40
|
||||
from os import environ
|
||||
|
||||
from django.apps.registry import Apps
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||
|
||||
|
||||
def create_default_user(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||
from django.contrib.auth.hashers import make_password
|
||||
|
||||
User = apps.get_model("authentik_core", "User")
|
||||
db_alias = schema_editor.connection.alias
|
||||
|
||||
akadmin, _ = User.objects.using(db_alias).get_or_create(
|
||||
username="akadmin", email="root@localhost", name="authentik Default Admin"
|
||||
)
|
||||
password = None
|
||||
if "TF_BUILD" in environ or settings.TEST:
|
||||
password = "akadmin" # noqa # nosec
|
||||
if "AK_ADMIN_PASS" in environ:
|
||||
password = environ["AK_ADMIN_PASS"]
|
||||
if "AUTHENTIK_BOOTSTRAP_PASSWORD" in environ:
|
||||
password = environ["AUTHENTIK_BOOTSTRAP_PASSWORD"]
|
||||
if password:
|
||||
akadmin.password = make_password(password)
|
||||
else:
|
||||
akadmin.password = make_password(None)
|
||||
akadmin.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_core", "0002_auto_20200523_1133"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name="user",
|
||||
name="is_superuser",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="user",
|
||||
name="is_staff",
|
||||
),
|
||||
migrations.RunPython(create_default_user),
|
||||
migrations.AddField(
|
||||
model_name="user",
|
||||
name="is_superuser",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="user", name="is_staff", field=models.BooleanField(default=False)
|
||||
),
|
||||
]
|
28
authentik/core/migrations/0004_auto_20200703_2213.py
Normal file
28
authentik/core/migrations/0004_auto_20200703_2213.py
Normal file
@ -0,0 +1,28 @@
|
||||
# Generated by Django 3.0.7 on 2020-07-03 22:13
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_core", "0003_default_user"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name="application",
|
||||
options={
|
||||
"verbose_name": "Application",
|
||||
"verbose_name_plural": "Applications",
|
||||
},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name="user",
|
||||
options={
|
||||
"permissions": (("reset_user_password", "Reset Password"),),
|
||||
"verbose_name": "User",
|
||||
"verbose_name_plural": "Users",
|
||||
},
|
||||
),
|
||||
]
|
24
authentik/core/migrations/0005_token_intent.py
Normal file
24
authentik/core/migrations/0005_token_intent.py
Normal file
@ -0,0 +1,24 @@
|
||||
# Generated by Django 3.0.7 on 2020-07-05 21:11
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_core", "0004_auto_20200703_2213"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="token",
|
||||
name="intent",
|
||||
field=models.TextField(
|
||||
choices=[
|
||||
("verification", "Intent Verification"),
|
||||
("api", "Intent Api"),
|
||||
],
|
||||
default="verification",
|
||||
),
|
||||
),
|
||||
]
|
18
authentik/core/migrations/0006_auto_20200709_1608.py
Normal file
18
authentik/core/migrations/0006_auto_20200709_1608.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.0.8 on 2020-07-09 16:08
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_core", "0005_token_intent"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="source",
|
||||
name="slug",
|
||||
field=models.SlugField(help_text="Internal source name, used in URLs.", unique=True),
|
||||
),
|
||||
]
|
18
authentik/core/migrations/0007_auto_20200815_1841.py
Normal file
18
authentik/core/migrations/0007_auto_20200815_1841.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.1 on 2020-08-15 18:41
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_core", "0006_auto_20200709_1608"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="user",
|
||||
name="first_name",
|
||||
field=models.CharField(blank=True, max_length=150, verbose_name="first name"),
|
||||
),
|
||||
]
|
36
authentik/core/migrations/0008_auto_20200824_1532.py
Normal file
36
authentik/core/migrations/0008_auto_20200824_1532.py
Normal file
@ -0,0 +1,36 @@
|
||||
# Generated by Django 3.1 on 2020-08-24 15:32
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("auth", "0012_alter_user_first_name_max_length"),
|
||||
("authentik_core", "0007_auto_20200815_1841"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name="user",
|
||||
name="groups",
|
||||
field=models.ManyToManyField(to="authentik_core.Group"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="user",
|
||||
name="groups",
|
||||
field=models.ManyToManyField(
|
||||
blank=True,
|
||||
help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.",
|
||||
related_name="user_set",
|
||||
related_query_name="user",
|
||||
to="auth.Group",
|
||||
verbose_name="groups",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="user",
|
||||
name="pb_groups",
|
||||
field=models.ManyToManyField(to="authentik_core.Group"),
|
||||
),
|
||||
]
|
59
authentik/core/migrations/0009_group_is_superuser.py
Normal file
59
authentik/core/migrations/0009_group_is_superuser.py
Normal file
@ -0,0 +1,59 @@
|
||||
# Generated by Django 3.1.1 on 2020-09-15 19:53
|
||||
from django.apps.registry import Apps
|
||||
from django.db import migrations, models
|
||||
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||
|
||||
import authentik.core.models
|
||||
|
||||
|
||||
def create_default_admin_group(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||
db_alias = schema_editor.connection.alias
|
||||
Group = apps.get_model("authentik_core", "Group")
|
||||
User = apps.get_model("authentik_core", "User")
|
||||
|
||||
# Creates a default admin group
|
||||
group, _ = Group.objects.using(db_alias).get_or_create(
|
||||
is_superuser=True,
|
||||
defaults={
|
||||
"name": "authentik Admins",
|
||||
},
|
||||
)
|
||||
group.users.set(User.objects.filter(username="akadmin"))
|
||||
group.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_core", "0008_auto_20200824_1532"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name="user",
|
||||
name="is_superuser",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="user",
|
||||
name="is_staff",
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="user",
|
||||
name="pb_groups",
|
||||
field=models.ManyToManyField(related_name="users", to="authentik_core.Group"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="group",
|
||||
name="is_superuser",
|
||||
field=models.BooleanField(
|
||||
default=False, help_text="Users added to this group will be superusers."
|
||||
),
|
||||
),
|
||||
migrations.RunPython(create_default_admin_group),
|
||||
migrations.AlterModelManagers(
|
||||
name="user",
|
||||
managers=[
|
||||
("objects", authentik.core.models.UserManager()),
|
||||
],
|
||||
),
|
||||
]
|
24
authentik/core/migrations/0010_auto_20200917_1021.py
Normal file
24
authentik/core/migrations/0010_auto_20200917_1021.py
Normal file
@ -0,0 +1,24 @@
|
||||
# Generated by Django 3.1.1 on 2020-09-17 10:21
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_core", "0009_group_is_superuser"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name="user",
|
||||
options={
|
||||
"permissions": (
|
||||
("reset_user_password", "Reset Password"),
|
||||
("impersonate", "Can impersonate other users"),
|
||||
),
|
||||
"verbose_name": "User",
|
||||
"verbose_name_plural": "Users",
|
||||
},
|
||||
),
|
||||
]
|
19
authentik/core/migrations/0011_provider_name_temp.py
Normal file
19
authentik/core/migrations/0011_provider_name_temp.py
Normal file
@ -0,0 +1,19 @@
|
||||
# Generated by Django 3.1.2 on 2020-10-03 17:34
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_core", "0010_auto_20200917_1021"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="provider",
|
||||
name="name_temp",
|
||||
field=models.TextField(default=""),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
20
authentik/core/migrations/0012_auto_20201003_1737.py
Normal file
20
authentik/core/migrations/0012_auto_20201003_1737.py
Normal file
@ -0,0 +1,20 @@
|
||||
# Generated by Django 3.1.2 on 2020-10-03 17:37
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_core", "0011_provider_name_temp"),
|
||||
("authentik_providers_oauth2", "0006_remove_oauth2provider_name"),
|
||||
("authentik_providers_saml", "0006_remove_samlprovider_name"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name="provider",
|
||||
old_name="name_temp",
|
||||
new_name="name",
|
||||
),
|
||||
]
|
35
authentik/core/migrations/0013_auto_20201003_2132.py
Normal file
35
authentik/core/migrations/0013_auto_20201003_2132.py
Normal file
@ -0,0 +1,35 @@
|
||||
# Generated by Django 3.1.2 on 2020-10-03 21:32
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_core", "0012_auto_20201003_1737"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="token",
|
||||
name="identifier",
|
||||
field=models.TextField(default=""),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="token",
|
||||
name="intent",
|
||||
field=models.TextField(
|
||||
choices=[
|
||||
("verification", "Intent Verification"),
|
||||
("api", "Intent Api"),
|
||||
("recovery", "Intent Recovery"),
|
||||
],
|
||||
default="verification",
|
||||
),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name="token",
|
||||
unique_together={("identifier", "user")},
|
||||
),
|
||||
]
|
48
authentik/core/migrations/0014_auto_20201018_1158.py
Normal file
48
authentik/core/migrations/0014_auto_20201018_1158.py
Normal file
@ -0,0 +1,48 @@
|
||||
# Generated by Django 3.1.2 on 2020-10-18 11:58
|
||||
from django.apps.registry import Apps
|
||||
from django.db import migrations, models
|
||||
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||
|
||||
import authentik.core.models
|
||||
|
||||
|
||||
def set_default_token_key(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||
db_alias = schema_editor.connection.alias
|
||||
Token = apps.get_model("authentik_core", "Token")
|
||||
|
||||
for token in Token.objects.using(db_alias).all():
|
||||
token.key = token.pk.hex
|
||||
token.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_core", "0013_auto_20201003_2132"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="token",
|
||||
name="key",
|
||||
field=models.TextField(default=authentik.core.models.default_token_key),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name="token",
|
||||
unique_together=set(),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="token",
|
||||
name="identifier",
|
||||
field=models.SlugField(max_length=255),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="token",
|
||||
index=models.Index(fields=["key"], name="authentik_co_key_e45007_idx"),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="token",
|
||||
index=models.Index(fields=["identifier"], name="authentik_co_identif_1a34a8_idx"),
|
||||
),
|
||||
migrations.RunPython(set_default_token_key),
|
||||
]
|
22
authentik/core/migrations/0015_application_icon.py
Normal file
22
authentik/core/migrations/0015_application_icon.py
Normal file
@ -0,0 +1,22 @@
|
||||
# Generated by Django 3.1.3 on 2020-11-23 17:19
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_core", "0014_auto_20201018_1158"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name="application",
|
||||
name="meta_icon_url",
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="application",
|
||||
name="meta_icon",
|
||||
field=models.FileField(blank=True, default="", upload_to="application-icons/"),
|
||||
),
|
||||
]
|
34
authentik/core/migrations/0016_auto_20201202_2234.py
Normal file
34
authentik/core/migrations/0016_auto_20201202_2234.py
Normal file
@ -0,0 +1,34 @@
|
||||
# Generated by Django 3.1.3 on 2020-12-02 22:34
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_core", "0015_application_icon"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveIndex(
|
||||
model_name="token",
|
||||
name="authentik_co_key_e45007_idx",
|
||||
),
|
||||
migrations.RemoveIndex(
|
||||
model_name="token",
|
||||
name="authentik_co_identif_1a34a8_idx",
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name="user",
|
||||
old_name="pb_groups",
|
||||
new_name="ak_groups",
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="token",
|
||||
index=models.Index(fields=["identifier"], name="authentik_c_identif_d9d032_idx"),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="token",
|
||||
index=models.Index(fields=["key"], name="authentik_c_key_f71355_idx"),
|
||||
),
|
||||
]
|
21
authentik/core/migrations/0018_auto_20210330_1345.py
Normal file
21
authentik/core/migrations/0018_auto_20210330_1345.py
Normal file
@ -0,0 +1,21 @@
|
||||
# Generated by Django 3.1.7 on 2021-03-30 13:45
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_core", "0017_managed"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name="token",
|
||||
options={
|
||||
"permissions": (("view_token_key", "View token's key"),),
|
||||
"verbose_name": "Token",
|
||||
"verbose_name_plural": "Tokens",
|
||||
},
|
||||
),
|
||||
]
|
24
authentik/core/migrations/0019_source_managed.py
Normal file
24
authentik/core/migrations/0019_source_managed.py
Normal file
@ -0,0 +1,24 @@
|
||||
# Generated by Django 3.2 on 2021-04-09 14:06
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_core", "0018_auto_20210330_1345"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="source",
|
||||
name="managed",
|
||||
field=models.TextField(
|
||||
default=None,
|
||||
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.",
|
||||
null=True,
|
||||
unique=True,
|
||||
verbose_name="Managed by authentik",
|
||||
),
|
||||
),
|
||||
]
|
40
authentik/core/migrations/0020_source_user_matching_mode.py
Normal file
40
authentik/core/migrations/0020_source_user_matching_mode.py
Normal file
@ -0,0 +1,40 @@
|
||||
# Generated by Django 3.2 on 2021-05-03 17:06
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_core", "0019_source_managed"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="source",
|
||||
name="user_matching_mode",
|
||||
field=models.TextField(
|
||||
choices=[
|
||||
("identifier", "Use the source-specific identifier"),
|
||||
(
|
||||
"email_link",
|
||||
"Link to a user with identical email address. Can have security implications when a source doesn't validate email addresses.",
|
||||
),
|
||||
(
|
||||
"email_deny",
|
||||
"Use the user's email address, but deny enrollment when the email address already exists.",
|
||||
),
|
||||
(
|
||||
"username_link",
|
||||
"Link to a user with identical username. Can have security implications when a username is used with another source.",
|
||||
),
|
||||
(
|
||||
"username_deny",
|
||||
"Use the user's username, but deny enrollment when the username already exists.",
|
||||
),
|
||||
],
|
||||
default="identifier",
|
||||
help_text="How the source determines if an existing user should be authenticated or a new user enrolled.",
|
||||
),
|
||||
),
|
||||
]
|
20
authentik/core/migrations/0021_alter_application_slug.py
Normal file
20
authentik/core/migrations/0021_alter_application_slug.py
Normal file
@ -0,0 +1,20 @@
|
||||
# Generated by Django 3.2.3 on 2021-05-14 08:48
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_core", "0020_source_user_matching_mode"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="application",
|
||||
name="slug",
|
||||
field=models.SlugField(
|
||||
help_text="Internal application name, used in URLs.", unique=True
|
||||
),
|
||||
),
|
||||
]
|
58
authentik/core/migrations/0022_authenticatedsession.py
Normal file
58
authentik/core/migrations/0022_authenticatedsession.py
Normal file
@ -0,0 +1,58 @@
|
||||
# Generated by Django 3.2.3 on 2021-05-29 22:14
|
||||
|
||||
import uuid
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.apps.registry import Apps
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||
|
||||
import authentik.core.models
|
||||
|
||||
|
||||
def migrate_sessions(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||
from django.contrib.sessions.backends.cache import KEY_PREFIX
|
||||
from django.core.cache import cache
|
||||
|
||||
session_keys = cache.keys(KEY_PREFIX + "*")
|
||||
cache.delete_many(session_keys)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_core", "0021_alter_application_slug"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="AuthenticatedSession",
|
||||
fields=[
|
||||
(
|
||||
"expires",
|
||||
models.DateTimeField(default=authentik.core.models.default_token_duration),
|
||||
),
|
||||
("expiring", models.BooleanField(default=True)),
|
||||
(
|
||||
"uuid",
|
||||
models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False),
|
||||
),
|
||||
("session_key", models.CharField(max_length=40)),
|
||||
("last_ip", models.TextField()),
|
||||
("last_user_agent", models.TextField(blank=True)),
|
||||
("last_used", models.DateTimeField(auto_now=True)),
|
||||
(
|
||||
"user",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"abstract": False,
|
||||
},
|
||||
),
|
||||
migrations.RunPython(migrate_sessions),
|
||||
]
|
@ -0,0 +1,24 @@
|
||||
# Generated by Django 3.2.3 on 2021-06-02 21:51
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
import authentik.lib.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_core", "0022_authenticatedsession"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="application",
|
||||
name="meta_launch_url",
|
||||
field=models.TextField(
|
||||
blank=True,
|
||||
default="",
|
||||
validators=[authentik.lib.models.DomainlessURLValidator()],
|
||||
),
|
||||
),
|
||||
]
|
@ -1,21 +0,0 @@
|
||||
# Generated by Django 4.1.2 on 2022-10-19 18:57
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_core", "0022_alter_group_parent"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddIndex(
|
||||
model_name="source",
|
||||
index=models.Index(fields=["slug"], name="authentik_c_slug_ccb2e5_idx"),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="source",
|
||||
index=models.Index(fields=["name"], name="authentik_c_name_affae6_idx"),
|
||||
),
|
||||
]
|
35
authentik/core/migrations/0024_alter_token_identifier.py
Normal file
35
authentik/core/migrations/0024_alter_token_identifier.py
Normal file
@ -0,0 +1,35 @@
|
||||
# Generated by Django 3.2.3 on 2021-06-03 09:33
|
||||
|
||||
from django.apps.registry import Apps
|
||||
from django.db import migrations, models
|
||||
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||
from django.db.models import Count
|
||||
|
||||
|
||||
def fix_duplicates(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||
db_alias = schema_editor.connection.alias
|
||||
Token = apps.get_model("authentik_core", "token")
|
||||
identifiers = (
|
||||
Token.objects.using(db_alias)
|
||||
.values("identifier")
|
||||
.annotate(identifier_count=Count("identifier"))
|
||||
.filter(identifier_count__gt=1)
|
||||
)
|
||||
for ident in identifiers:
|
||||
Token.objects.using(db_alias).filter(identifier=ident["identifier"]).delete()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_core", "0023_alter_application_meta_launch_url"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(fix_duplicates),
|
||||
migrations.AlterField(
|
||||
model_name="token",
|
||||
name="identifier",
|
||||
field=models.SlugField(max_length=255, unique=True),
|
||||
),
|
||||
]
|
@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.2.3 on 2021-06-05 19:04
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_core", "0024_alter_token_identifier"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="application",
|
||||
name="meta_icon",
|
||||
field=models.FileField(default=None, null=True, upload_to="application-icons/"),
|
||||
),
|
||||
]
|
@ -0,0 +1,27 @@
|
||||
# Generated by Django 3.2.5 on 2021-07-09 17:27
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_core", "0025_alter_application_meta_icon"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="application",
|
||||
name="meta_icon",
|
||||
field=models.FileField(
|
||||
default=None, max_length=500, null=True, upload_to="application-icons/"
|
||||
),
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name="authenticatedsession",
|
||||
options={
|
||||
"verbose_name": "Authenticated Session",
|
||||
"verbose_name_plural": "Authenticated Sessions",
|
||||
},
|
||||
),
|
||||
]
|
44
authentik/core/migrations/0027_bootstrap_token.py
Normal file
44
authentik/core/migrations/0027_bootstrap_token.py
Normal file
@ -0,0 +1,44 @@
|
||||
# Generated by Django 3.2.5 on 2021-08-11 19:40
|
||||
from os import environ
|
||||
|
||||
from django.apps.registry import Apps
|
||||
from django.db import migrations
|
||||
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||
|
||||
|
||||
def create_default_user_token(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||
from authentik.core.models import TokenIntents
|
||||
|
||||
User = apps.get_model("authentik_core", "User")
|
||||
Token = apps.get_model("authentik_core", "Token")
|
||||
|
||||
db_alias = schema_editor.connection.alias
|
||||
|
||||
akadmin = User.objects.using(db_alias).filter(username="akadmin")
|
||||
if not akadmin.exists():
|
||||
return
|
||||
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
|
||||
Token.objects.using(db_alias).create(
|
||||
identifier="authentik-bootstrap-token",
|
||||
user=akadmin.first(),
|
||||
intent=TokenIntents.INTENT_API,
|
||||
expiring=False,
|
||||
key=key,
|
||||
)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_core", "0026_alter_application_meta_icon"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(create_default_user_token),
|
||||
]
|
26
authentik/core/migrations/0028_alter_token_intent.py
Normal file
26
authentik/core/migrations/0028_alter_token_intent.py
Normal file
@ -0,0 +1,26 @@
|
||||
# Generated by Django 3.2.6 on 2021-08-23 14:35
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_core", "0027_bootstrap_token"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="token",
|
||||
name="intent",
|
||||
field=models.TextField(
|
||||
choices=[
|
||||
("verification", "Intent Verification"),
|
||||
("api", "Intent Api"),
|
||||
("recovery", "Intent Recovery"),
|
||||
("app_password", "Intent App Password"),
|
||||
],
|
||||
default="verification",
|
||||
),
|
||||
),
|
||||
]
|
@ -220,17 +220,6 @@ class User(SerializerModel, GuardianUserMixin, AbstractUser):
|
||||
"""Generate a globally unique UID, based on the user ID and the hashed secret key"""
|
||||
return sha256(f"{self.id}-{settings.SECRET_KEY}".encode("ascii")).hexdigest()
|
||||
|
||||
def locale(self, request: Optional[HttpRequest] = None) -> str:
|
||||
"""Get the locale the user has configured"""
|
||||
try:
|
||||
return self.attributes.get("settings", {}).get("locale", "")
|
||||
# pylint: disable=broad-except
|
||||
except Exception as exc:
|
||||
LOGGER.warning("Failed to get default locale", exc=exc)
|
||||
if request:
|
||||
return request.tenant.locale
|
||||
return ""
|
||||
|
||||
@property
|
||||
def avatar(self) -> str:
|
||||
"""Get avatar, depending on authentik.avatar setting"""
|
||||
@ -483,21 +472,6 @@ class Source(ManagedModel, SerializerModel, PolicyBindingModel):
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class Meta:
|
||||
|
||||
indexes = [
|
||||
models.Index(
|
||||
fields=[
|
||||
"slug",
|
||||
]
|
||||
),
|
||||
models.Index(
|
||||
fields=[
|
||||
"name",
|
||||
]
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
class UserSourceConnection(SerializerModel, CreatedUpdatedModel):
|
||||
"""Connection between User and Source."""
|
||||
@ -643,9 +617,10 @@ class PropertyMapping(SerializerModel, ManagedModel):
|
||||
|
||||
def evaluate(self, user: Optional[User], request: Optional[HttpRequest], **kwargs) -> Any:
|
||||
"""Evaluate `self.expression` using `**kwargs` as Context."""
|
||||
from authentik.core.expression.evaluator import PropertyMappingEvaluator
|
||||
from authentik.core.expression import PropertyMappingEvaluator
|
||||
|
||||
evaluator = PropertyMappingEvaluator(self, user, request, **kwargs)
|
||||
evaluator = PropertyMappingEvaluator()
|
||||
evaluator.set_context(user, request, self, **kwargs)
|
||||
try:
|
||||
return evaluator.evaluate(self.expression)
|
||||
except Exception as exc:
|
||||
|
@ -5,7 +5,7 @@ from typing import Any, Optional
|
||||
from django.contrib import messages
|
||||
from django.db import IntegrityError
|
||||
from django.db.models.query_utils import Q
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.http import HttpRequest, HttpResponse, HttpResponseBadRequest
|
||||
from django.shortcuts import redirect
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext as _
|
||||
@ -23,10 +23,8 @@ from authentik.flows.planner import (
|
||||
PLAN_CONTEXT_SSO,
|
||||
FlowPlanner,
|
||||
)
|
||||
from authentik.flows.stage import StageView
|
||||
from authentik.flows.views.executor import NEXT_ARG_NAME, SESSION_KEY_GET, SESSION_KEY_PLAN
|
||||
from authentik.lib.utils.urls import redirect_with_qs
|
||||
from authentik.lib.views import bad_request_message
|
||||
from authentik.policies.denied import AccessDeniedResponse
|
||||
from authentik.policies.utils import delete_none_keys
|
||||
from authentik.stages.password import BACKEND_INBUILT
|
||||
@ -45,26 +43,6 @@ class Action(Enum):
|
||||
DENY = "deny"
|
||||
|
||||
|
||||
class MessageStage(StageView):
|
||||
"""Show a pre-configured message after the flow is done"""
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||
"""Show a pre-configured message after the flow is done"""
|
||||
message = getattr(self.executor.current_stage, "message", "")
|
||||
level = getattr(self.executor.current_stage, "level", messages.SUCCESS)
|
||||
messages.add_message(
|
||||
self.request,
|
||||
level,
|
||||
message,
|
||||
)
|
||||
return self.executor.stage_ok()
|
||||
|
||||
def post(self, request: HttpRequest) -> HttpResponse:
|
||||
"""Wrapper for post requests"""
|
||||
return self.get(request)
|
||||
|
||||
|
||||
class SourceFlowManager:
|
||||
"""Help sources decide what they should do after authorization. Based on source settings and
|
||||
previous connections, authenticate the user, enroll a new user, link to an existing user
|
||||
@ -172,16 +150,16 @@ class SourceFlowManager:
|
||||
action, connection = self.get_action(**kwargs)
|
||||
except IntegrityError as exc:
|
||||
self._logger.warning("failed to get action", exc=exc)
|
||||
return redirect(reverse("authentik_core:root-redirect"))
|
||||
return redirect("/")
|
||||
self._logger.debug("get_action", action=action, connection=connection)
|
||||
try:
|
||||
if connection:
|
||||
if action == Action.LINK:
|
||||
self._logger.debug("Linking existing user")
|
||||
return self.handle_existing_link(connection)
|
||||
return self.handle_existing_user_link(connection)
|
||||
if action == Action.AUTH:
|
||||
self._logger.debug("Handling auth user")
|
||||
return self.handle_auth(connection)
|
||||
return self.handle_auth_user(connection)
|
||||
if action == Action.ENROLL:
|
||||
self._logger.debug("Handling enrollment of new user")
|
||||
return self.handle_enroll(connection)
|
||||
@ -220,12 +198,8 @@ class SourceFlowManager:
|
||||
]
|
||||
return []
|
||||
|
||||
def _prepare_flow(
|
||||
self,
|
||||
flow: Flow,
|
||||
connection: UserSourceConnection,
|
||||
stages: Optional[list[StageView]] = None,
|
||||
**kwargs,
|
||||
def _handle_login_flow(
|
||||
self, flow: Flow, connection: UserSourceConnection, **kwargs
|
||||
) -> HttpResponse:
|
||||
"""Prepare Authentication Plan, redirect user FlowExecutor"""
|
||||
# Ensure redirect is carried through when user was trying to
|
||||
@ -245,18 +219,12 @@ class SourceFlowManager:
|
||||
)
|
||||
kwargs.update(self.policy_context)
|
||||
if not flow:
|
||||
return bad_request_message(
|
||||
self.request,
|
||||
_("Configured flow does not exist."),
|
||||
)
|
||||
return HttpResponseBadRequest()
|
||||
# We run the Flow planner here so we can pass the Pending user in the context
|
||||
planner = FlowPlanner(flow)
|
||||
plan = planner.plan(self.request, kwargs)
|
||||
for stage in self.get_stages_to_append(flow):
|
||||
plan.append_stage(stage)
|
||||
if stages:
|
||||
for stage in stages:
|
||||
plan.append_stage(stage)
|
||||
plan.append_stage(stage=stage)
|
||||
self.request.session[SESSION_KEY_PLAN] = plan
|
||||
return redirect_with_qs(
|
||||
"authentik_core:if-flow",
|
||||
@ -265,35 +233,24 @@ class SourceFlowManager:
|
||||
)
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def handle_auth(
|
||||
def handle_auth_user(
|
||||
self,
|
||||
connection: UserSourceConnection,
|
||||
) -> HttpResponse:
|
||||
"""Login user and redirect."""
|
||||
flow_kwargs = {PLAN_CONTEXT_PENDING_USER: connection.user}
|
||||
return self._prepare_flow(
|
||||
self.source.authentication_flow,
|
||||
connection,
|
||||
stages=[
|
||||
in_memory_stage(
|
||||
MessageStage,
|
||||
message=_(
|
||||
"Successfully authenticated with %(source)s!" % {"source": self.source.name}
|
||||
),
|
||||
)
|
||||
],
|
||||
**flow_kwargs,
|
||||
messages.success(
|
||||
self.request,
|
||||
_("Successfully authenticated with %(source)s!" % {"source": self.source.name}),
|
||||
)
|
||||
flow_kwargs = {PLAN_CONTEXT_PENDING_USER: connection.user}
|
||||
return self._handle_login_flow(self.source.authentication_flow, connection, **flow_kwargs)
|
||||
|
||||
def handle_existing_link(
|
||||
def handle_existing_user_link(
|
||||
self,
|
||||
connection: UserSourceConnection,
|
||||
) -> HttpResponse:
|
||||
"""Handler when the user was already authenticated and linked an external source
|
||||
to their account."""
|
||||
# When request isn't authenticated we jump straight to auth
|
||||
if not self.request.user.is_authenticated:
|
||||
return self.handle_auth(connection)
|
||||
# Connection has already been saved
|
||||
Event.new(
|
||||
EventAction.SOURCE_LINKED,
|
||||
@ -304,6 +261,9 @@ class SourceFlowManager:
|
||||
self.request,
|
||||
_("Successfully linked %(source)s!" % {"source": self.source.name}),
|
||||
)
|
||||
# When request isn't authenticated we jump straight to auth
|
||||
if not self.request.user.is_authenticated:
|
||||
return self.handle_auth_user(connection)
|
||||
return redirect(
|
||||
reverse(
|
||||
"authentik_core:if-user",
|
||||
@ -316,24 +276,18 @@ class SourceFlowManager:
|
||||
connection: UserSourceConnection,
|
||||
) -> HttpResponse:
|
||||
"""User was not authenticated and previous request was not authenticated."""
|
||||
messages.success(
|
||||
self.request,
|
||||
_("Successfully authenticated with %(source)s!" % {"source": self.source.name}),
|
||||
)
|
||||
|
||||
# We run the Flow planner here so we can pass the Pending user in the context
|
||||
if not self.source.enrollment_flow:
|
||||
self._logger.warning("source has no enrollment flow")
|
||||
return bad_request_message(
|
||||
self.request,
|
||||
_("Source is not configured for enrollment."),
|
||||
)
|
||||
return self._prepare_flow(
|
||||
return HttpResponseBadRequest()
|
||||
return self._handle_login_flow(
|
||||
self.source.enrollment_flow,
|
||||
connection,
|
||||
stages=[
|
||||
in_memory_stage(
|
||||
MessageStage,
|
||||
message=_(
|
||||
"Successfully authenticated with %(source)s!" % {"source": self.source.name}
|
||||
),
|
||||
)
|
||||
],
|
||||
**{
|
||||
PLAN_CONTEXT_PROMPT: delete_none_keys(self.enroll_info),
|
||||
PLAN_CONTEXT_USER_PATH: self.source.get_user_path(),
|
||||
|
@ -1,23 +0,0 @@
|
||||
{% load i18n %}
|
||||
{% get_current_language as LANGUAGE_CODE %}
|
||||
|
||||
<script>
|
||||
window.authentik = {};
|
||||
window.authentik.locale = "{{ LANGUAGE_CODE }}";
|
||||
window.authentik.config = JSON.parse('{{ config_json|escapejs }}');
|
||||
window.authentik.tenant = JSON.parse('{{ tenant_json|escapejs }}');
|
||||
window.addEventListener("DOMContentLoaded", () => {
|
||||
{% for message in messages %}
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("ak-message", {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
detail: {
|
||||
level: "{{ message.tags|escapejs }}",
|
||||
message: "{{ message.message|escapejs }}",
|
||||
},
|
||||
}),
|
||||
);
|
||||
{% endfor %}
|
||||
});
|
||||
</script>
|
31
authentik/core/templates/error/generic.html
Normal file
31
authentik/core/templates/error/generic.html
Normal file
@ -0,0 +1,31 @@
|
||||
{% extends 'base/skeleton.html' %}
|
||||
|
||||
{% load i18n %}
|
||||
|
||||
{% block head %}
|
||||
{{ block.super }}
|
||||
<style>
|
||||
.pf-c-empty-state {
|
||||
height: 100vh;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<section class="ak-static-page pf-c-page__main-section pf-m-no-padding-mobile pf-m-xl">
|
||||
<div class="pf-c-empty-state">
|
||||
<div class="pf-c-empty-state__content">
|
||||
<i class="fas fa-exclamation-circle pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
<h1 class="pf-c-title pf-m-lg">
|
||||
{% trans title %}
|
||||
</h1>
|
||||
<div class="pf-c-empty-state__body">
|
||||
{% if message %}
|
||||
<h3>{% trans message %}</h3>
|
||||
{% endif %}
|
||||
</div>
|
||||
<a href="/" class="pf-c-button pf-m-primary pf-m-block">{% trans 'Go to home' %}</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
@ -4,17 +4,20 @@
|
||||
{% load i18n %}
|
||||
|
||||
{% block head %}
|
||||
<script src="{% static 'dist/admin/AdminInterface.js' %}?version={{ version }}" type="module"></script>
|
||||
<script src="{% static 'dist/admin/AdminInterface.js' %}" type="module"></script>
|
||||
<meta name="theme-color" content="#18191a" media="(prefers-color-scheme: dark)">
|
||||
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">
|
||||
<link rel="icon" href="{{ tenant.branding_favicon }}">
|
||||
<link rel="shortcut icon" href="{{ tenant.branding_favicon }}">
|
||||
{% include "base/header_js.html" %}
|
||||
<script>
|
||||
window.authentik = {};
|
||||
window.authentik.locale = "{{ tenant.default_locale }}";
|
||||
window.authentik.config = JSON.parse('{{ config_json|escapejs }}');
|
||||
window.authentik.tenant = JSON.parse('{{ tenant_json|escapejs }}');
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<ak-message-container></ak-message-container>
|
||||
<ak-interface-admin>
|
||||
<ak-message-container data-refresh-on-locale="true"></ak-message-container>
|
||||
<ak-interface-admin data-refresh-on-locale="true">
|
||||
<section class="ak-static-page pf-c-page__main-section pf-m-no-padding-mobile pf-m-xl">
|
||||
<div class="pf-c-empty-state" style="height: 100vh;">
|
||||
<div class="pf-c-empty-state__content">
|
||||
|
@ -1,21 +0,0 @@
|
||||
{% extends 'login/base_full.html' %}
|
||||
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}
|
||||
{{ tenant.branding_title }}
|
||||
{% endblock %}
|
||||
|
||||
{% block card_title %}
|
||||
{% trans title %}
|
||||
{% endblock %}
|
||||
|
||||
{% block card %}
|
||||
<form method="POST" class="pf-c-form">
|
||||
<p>{% trans message %}</p>
|
||||
<a id="ak-back-home" href="{% url 'authentik_core:root-redirect' %}" class="pf-c-button pf-m-primary">
|
||||
{% trans 'Go home' %}
|
||||
</a>
|
||||
</form>
|
||||
{% endblock %}
|
@ -6,13 +6,14 @@
|
||||
{% block head_before %}
|
||||
{{ block.super }}
|
||||
<link rel="prefetch" href="{{ flow.background_url }}" />
|
||||
<link rel="icon" href="{{ tenant.branding_favicon }}">
|
||||
<link rel="shortcut icon" href="{{ tenant.branding_favicon }}">
|
||||
{% if flow.compatibility_mode and not inspector %}
|
||||
<script>ShadyDOM = { force: !navigator.webdriver };</script>
|
||||
{% endif %}
|
||||
{% include "base/header_js.html" %}
|
||||
<script>
|
||||
window.authentik = {};
|
||||
window.authentik.locale = "{{ tenant.default_locale }}";
|
||||
window.authentik.config = JSON.parse('{{ config_json|escapejs }}');
|
||||
window.authentik.tenant = JSON.parse('{{ tenant_json|escapejs }}');
|
||||
window.authentik.flow = {
|
||||
"layout": "{{ flow.layout }}",
|
||||
};
|
||||
@ -20,7 +21,7 @@ window.authentik.flow = {
|
||||
{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<script src="{% static 'dist/flow/FlowInterface.js' %}?version={{ version }}" type="module"></script>
|
||||
<script src="{% static 'dist/flow/FlowInterface.js' %}" type="module"></script>
|
||||
<style>
|
||||
:root {
|
||||
--ak-flow-background: url("{{ flow.background_url }}");
|
||||
@ -29,8 +30,8 @@ window.authentik.flow = {
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<ak-message-container></ak-message-container>
|
||||
<ak-flow-executor>
|
||||
<ak-message-container data-refresh-on-locale="true"></ak-message-container>
|
||||
<ak-flow-executor data-refresh-on-locale="true">
|
||||
<section class="ak-static-page pf-c-page__main-section pf-m-no-padding-mobile pf-m-xl">
|
||||
<div class="pf-c-empty-state" style="height: 100vh;">
|
||||
<div class="pf-c-empty-state__content">
|
||||
|
@ -4,17 +4,20 @@
|
||||
{% load i18n %}
|
||||
|
||||
{% block head %}
|
||||
<script src="{% static 'dist/user/UserInterface.js' %}?version={{ version }}" type="module"></script>
|
||||
<script src="{% static 'dist/user/UserInterface.js' %}" type="module"></script>
|
||||
<meta name="theme-color" content="#151515" media="(prefers-color-scheme: light)">
|
||||
<meta name="theme-color" content="#151515" media="(prefers-color-scheme: dark)">
|
||||
<link rel="icon" href="{{ tenant.branding_favicon }}">
|
||||
<link rel="shortcut icon" href="{{ tenant.branding_favicon }}">
|
||||
{% include "base/header_js.html" %}
|
||||
<script>
|
||||
window.authentik = {};
|
||||
window.authentik.locale = "{{ tenant.default_locale }}";
|
||||
window.authentik.config = JSON.parse('{{ config_json|escapejs }}');
|
||||
window.authentik.tenant = JSON.parse('{{ tenant_json|escapejs }}');
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<ak-message-container></ak-message-container>
|
||||
<ak-interface-user>
|
||||
<ak-message-container data-refresh-on-locale="true"></ak-message-container>
|
||||
<ak-interface-user data-refresh-on-locale="true">
|
||||
<section class="ak-static-page pf-c-page__main-section pf-m-no-padding-mobile pf-m-xl">
|
||||
<div class="pf-c-empty-state" style="height: 100vh;">
|
||||
<div class="pf-c-empty-state__content">
|
||||
|
@ -6,7 +6,6 @@
|
||||
{% block head_before %}
|
||||
<link rel="prefetch" href="/static/dist/assets/images/flow_background.jpg" />
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/patternfly.min.css' %}">
|
||||
{% include "base/header_js.html" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
|
@ -5,7 +5,8 @@ from django.urls import reverse
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from authentik.core.models import Application
|
||||
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
|
||||
from authentik.core.tests.utils import create_test_admin_user
|
||||
from authentik.flows.models import Flow
|
||||
from authentik.policies.dummy.models import DummyPolicy
|
||||
from authentik.policies.models import PolicyBinding
|
||||
from authentik.providers.oauth2.models import OAuth2Provider
|
||||
@ -19,7 +20,10 @@ class TestApplicationsAPI(APITestCase):
|
||||
self.provider = OAuth2Provider.objects.create(
|
||||
name="test",
|
||||
redirect_uris="http://some-other-domain",
|
||||
authorization_flow=create_test_flow(),
|
||||
authorization_flow=Flow.objects.create(
|
||||
name="test",
|
||||
slug="test",
|
||||
),
|
||||
)
|
||||
self.allowed = Application.objects.create(
|
||||
name="allowed",
|
||||
|
@ -4,7 +4,8 @@ from unittest.mock import MagicMock, patch
|
||||
from django.urls import reverse
|
||||
|
||||
from authentik.core.models import Application
|
||||
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_tenant
|
||||
from authentik.flows.models import Flow, FlowDesignation
|
||||
from authentik.flows.tests import FlowTestCase
|
||||
from authentik.tenants.models import Tenant
|
||||
|
||||
@ -20,7 +21,11 @@ class TestApplicationsViews(FlowTestCase):
|
||||
|
||||
def test_check_redirect(self):
|
||||
"""Test redirect"""
|
||||
empty_flow = create_test_flow()
|
||||
empty_flow = Flow.objects.create(
|
||||
name="foo",
|
||||
slug="foo",
|
||||
designation=FlowDesignation.AUTHENTICATION,
|
||||
)
|
||||
tenant: Tenant = create_test_tenant()
|
||||
tenant.flow_authentication = empty_flow
|
||||
tenant.save()
|
||||
@ -44,7 +49,11 @@ class TestApplicationsViews(FlowTestCase):
|
||||
def test_check_redirect_auth(self):
|
||||
"""Test redirect"""
|
||||
self.client.force_login(self.user)
|
||||
empty_flow = create_test_flow()
|
||||
empty_flow = Flow.objects.create(
|
||||
name="foo",
|
||||
slug="foo",
|
||||
designation=FlowDesignation.AUTHENTICATION,
|
||||
)
|
||||
tenant: Tenant = create_test_tenant()
|
||||
tenant.flow_authentication = empty_flow
|
||||
tenant.save()
|
||||
|
@ -6,7 +6,7 @@ from guardian.utils import get_anonymous_user
|
||||
|
||||
from authentik.core.models import SourceUserMatchingModes, User
|
||||
from authentik.core.sources.flow_manager import Action
|
||||
from authentik.core.tests.utils import create_test_flow
|
||||
from authentik.flows.models import Flow, FlowDesignation
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.lib.tests.utils import get_request
|
||||
from authentik.policies.denied import AccessDeniedResponse
|
||||
@ -152,7 +152,9 @@ class TestSourceFlowManager(TestCase):
|
||||
"""Test error handling when a source selected flow is non-applicable due to a policy"""
|
||||
self.source.user_matching_mode = SourceUserMatchingModes.USERNAME_LINK
|
||||
|
||||
flow = create_test_flow()
|
||||
flow = Flow.objects.create(
|
||||
name="test", slug="test", title="test", designation=FlowDesignation.ENROLLMENT
|
||||
)
|
||||
policy = ExpressionPolicy.objects.create(
|
||||
name="false", expression="""ak_message("foo");return False"""
|
||||
)
|
||||
|
@ -159,6 +159,7 @@ class TestUsersAPI(APITestCase):
|
||||
response = self.client.get(
|
||||
reverse("authentik_api:user-paths"),
|
||||
)
|
||||
print(response.content)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertJSONEqual(response.content.decode(), {"paths": ["users"]})
|
||||
|
||||
|
@ -44,15 +44,13 @@ def create_test_tenant() -> Tenant:
|
||||
return Tenant.objects.create(domain=uid, default=True)
|
||||
|
||||
|
||||
def create_test_cert(use_ec_private_key=False) -> CertificateKeyPair:
|
||||
def create_test_cert() -> CertificateKeyPair:
|
||||
"""Generate a certificate for testing"""
|
||||
builder = CertificateBuilder(
|
||||
use_ec_private_key=use_ec_private_key,
|
||||
)
|
||||
builder = CertificateBuilder()
|
||||
builder.common_name = "goauthentik.io"
|
||||
builder.build(
|
||||
subject_alt_names=["goauthentik.io"],
|
||||
validity_days=360,
|
||||
)
|
||||
builder.common_name = generate_id()
|
||||
builder.name = generate_id()
|
||||
return builder.save()
|
||||
|
@ -32,7 +32,7 @@ class BadRequestView(TemplateView):
|
||||
extra_context = {"title": "Bad Request"}
|
||||
|
||||
response_class = BadRequestTemplateResponse
|
||||
template_name = "if/error.html"
|
||||
template_name = "error/generic.html"
|
||||
|
||||
|
||||
class ForbiddenView(TemplateView):
|
||||
@ -41,7 +41,7 @@ class ForbiddenView(TemplateView):
|
||||
extra_context = {"title": "Forbidden"}
|
||||
|
||||
response_class = ForbiddenTemplateResponse
|
||||
template_name = "if/error.html"
|
||||
template_name = "error/generic.html"
|
||||
|
||||
|
||||
class NotFoundView(TemplateView):
|
||||
@ -50,7 +50,7 @@ class NotFoundView(TemplateView):
|
||||
extra_context = {"title": "Not Found"}
|
||||
|
||||
response_class = NotFoundTemplateResponse
|
||||
template_name = "if/error.html"
|
||||
template_name = "error/generic.html"
|
||||
|
||||
|
||||
class ServerErrorView(TemplateView):
|
||||
@ -59,7 +59,7 @@ class ServerErrorView(TemplateView):
|
||||
extra_context = {"title": "Server Error"}
|
||||
|
||||
response_class = ServerErrorTemplateResponse
|
||||
template_name = "if/error.html"
|
||||
template_name = "error/generic.html"
|
||||
|
||||
# pylint: disable=useless-super-delegation
|
||||
def dispatch(self, *args, **kwargs): # pragma: no cover
|
||||
|
@ -1,5 +1,4 @@
|
||||
"""Crypto API Views"""
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
@ -13,11 +12,10 @@ from django_filters.filters import BooleanFilter
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.fields import CharField, DateTimeField, IntegerField, SerializerMethodField
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.serializers import ModelSerializer
|
||||
from rest_framework.serializers import ModelSerializer, ValidationError
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
@ -35,10 +33,7 @@ LOGGER = get_logger()
|
||||
class CertificateKeyPairSerializer(ModelSerializer):
|
||||
"""CertificateKeyPair Serializer"""
|
||||
|
||||
fingerprint_sha256 = SerializerMethodField()
|
||||
fingerprint_sha1 = SerializerMethodField()
|
||||
|
||||
cert_expiry = SerializerMethodField()
|
||||
cert_expiry = DateTimeField(source="certificate.not_valid_after", read_only=True)
|
||||
cert_subject = SerializerMethodField()
|
||||
private_key_available = SerializerMethodField()
|
||||
private_key_type = SerializerMethodField()
|
||||
@ -46,35 +41,8 @@ class CertificateKeyPairSerializer(ModelSerializer):
|
||||
certificate_download_url = SerializerMethodField()
|
||||
private_key_download_url = SerializerMethodField()
|
||||
|
||||
@property
|
||||
def _should_include_details(self) -> bool:
|
||||
request: Request = self.context.get("request", None)
|
||||
if not request:
|
||||
return True
|
||||
return str(request.query_params.get("include_details", "true")).lower() == "true"
|
||||
|
||||
def get_fingerprint_sha256(self, instance: CertificateKeyPair) -> Optional[str]:
|
||||
"Get certificate Hash (SHA256)"
|
||||
if not self._should_include_details:
|
||||
return None
|
||||
return instance.fingerprint_sha256
|
||||
|
||||
def get_fingerprint_sha1(self, instance: CertificateKeyPair) -> Optional[str]:
|
||||
"Get certificate Hash (SHA1)"
|
||||
if not self._should_include_details:
|
||||
return None
|
||||
return instance.fingerprint_sha1
|
||||
|
||||
def get_cert_expiry(self, instance: CertificateKeyPair) -> Optional[datetime]:
|
||||
"Get certificate expiry"
|
||||
if not self._should_include_details:
|
||||
return None
|
||||
return DateTimeField().to_representation(instance.certificate.not_valid_after)
|
||||
|
||||
def get_cert_subject(self, instance: CertificateKeyPair) -> Optional[str]:
|
||||
def get_cert_subject(self, instance: CertificateKeyPair) -> str:
|
||||
"""Get certificate subject as full rfc4514"""
|
||||
if not self._should_include_details:
|
||||
return None
|
||||
return instance.certificate.subject.rfc4514_string()
|
||||
|
||||
def get_private_key_available(self, instance: CertificateKeyPair) -> bool:
|
||||
@ -83,8 +51,6 @@ class CertificateKeyPairSerializer(ModelSerializer):
|
||||
|
||||
def get_private_key_type(self, instance: CertificateKeyPair) -> Optional[str]:
|
||||
"""Get the private key's type, if set"""
|
||||
if not self._should_include_details:
|
||||
return None
|
||||
key = instance.private_key
|
||||
if key:
|
||||
return key.__class__.__name__.replace("_", "").lower().replace("privatekey", "")
|
||||
@ -204,14 +170,6 @@ class CertificateKeyPairViewSet(UsedByMixin, ModelViewSet):
|
||||
ordering = ["name"]
|
||||
search_fields = ["name"]
|
||||
|
||||
@extend_schema(
|
||||
parameters=[
|
||||
OpenApiParameter("include_details", bool, default=True),
|
||||
]
|
||||
)
|
||||
def list(self, request, *args, **kwargs):
|
||||
return super().list(request, *args, **kwargs)
|
||||
|
||||
@permission_required(None, ["authentik_crypto.add_certificatekeypair"])
|
||||
@extend_schema(
|
||||
request=CertificateGenerationSerializer(),
|
||||
|
@ -2,7 +2,7 @@
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from authentik.blueprints.apps import ManagedAppConfig
|
||||
from authentik.blueprints.manager import ManagedAppConfig
|
||||
from authentik.lib.generators import generate_id
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
@ -6,8 +6,7 @@ from typing import Optional
|
||||
from cryptography import x509
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives import hashes, serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import ec, rsa
|
||||
from cryptography.hazmat.primitives.asymmetric.types import PRIVATE_KEY_TYPES
|
||||
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||
from cryptography.x509.oid import NameOID
|
||||
|
||||
from authentik import __version__
|
||||
@ -19,10 +18,7 @@ class CertificateBuilder:
|
||||
|
||||
common_name: str
|
||||
|
||||
_use_ec_private_key: bool
|
||||
|
||||
def __init__(self, use_ec_private_key=False):
|
||||
self._use_ec_private_key = use_ec_private_key
|
||||
def __init__(self):
|
||||
self.__public_key = None
|
||||
self.__private_key = None
|
||||
self.__builder = None
|
||||
@ -30,7 +26,7 @@ class CertificateBuilder:
|
||||
self.common_name = "authentik Self-signed Certificate"
|
||||
self.cert = CertificateKeyPair()
|
||||
|
||||
def save(self) -> CertificateKeyPair:
|
||||
def save(self) -> Optional[CertificateKeyPair]:
|
||||
"""Save generated certificate as model"""
|
||||
if not self.__certificate:
|
||||
raise ValueError("Certificated hasn't been built yet")
|
||||
@ -40,14 +36,6 @@ class CertificateBuilder:
|
||||
self.cert.save()
|
||||
return self.cert
|
||||
|
||||
def generate_private_key(self) -> PRIVATE_KEY_TYPES:
|
||||
"""Generate private key"""
|
||||
if self._use_ec_private_key:
|
||||
return ec.generate_private_key(curve=ec.SECP256R1)
|
||||
return rsa.generate_private_key(
|
||||
public_exponent=65537, key_size=4096, backend=default_backend()
|
||||
)
|
||||
|
||||
def build(
|
||||
self,
|
||||
validity_days: int = 365,
|
||||
@ -55,7 +43,9 @@ class CertificateBuilder:
|
||||
):
|
||||
"""Build self-signed certificate"""
|
||||
one_day = datetime.timedelta(1, 0, 0)
|
||||
self.__private_key = self.generate_private_key()
|
||||
self.__private_key = rsa.generate_private_key(
|
||||
public_exponent=65537, key_size=4096, backend=default_backend()
|
||||
)
|
||||
self.__public_key = self.__private_key.public_key()
|
||||
alt_names: list[x509.GeneralName] = [x509.DNSName(x) for x in subject_alt_names or []]
|
||||
self.__builder = (
|
||||
|
@ -1,51 +0,0 @@
|
||||
"""Import certificate"""
|
||||
from sys import exit as sys_exit
|
||||
|
||||
from django.core.management.base import BaseCommand, no_translations
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.crypto.api import CertificateKeyPairSerializer
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
"""Import certificate"""
|
||||
|
||||
@no_translations
|
||||
def handle(self, *args, **options):
|
||||
"""Import certificate"""
|
||||
keypair = CertificateKeyPair.objects.filter(name=options["name"]).first()
|
||||
dirty = False
|
||||
if not keypair:
|
||||
keypair = CertificateKeyPair(name=options["name"])
|
||||
dirty = True
|
||||
with open(options["certificate"], mode="r", encoding="utf-8") as _cert:
|
||||
cert_data = _cert.read()
|
||||
if keypair.certificate_data != cert_data:
|
||||
dirty = True
|
||||
keypair.certificate_data = cert_data
|
||||
if options["private_key"]:
|
||||
with open(options["private_key"], mode="r", encoding="utf-8") as _key:
|
||||
key_data = _key.read()
|
||||
if keypair.key_data != key_data:
|
||||
dirty = True
|
||||
keypair.key_data = key_data
|
||||
# Validate that cert and key are actually PEM and valid
|
||||
serializer = CertificateKeyPairSerializer(instance=keypair)
|
||||
try:
|
||||
serializer.validate_certificate_data(keypair.certificate_data)
|
||||
if keypair.key_data != "":
|
||||
serializer.validate_key_data(keypair.key_data)
|
||||
except ValidationError as exc:
|
||||
self.stderr.write(str(exc))
|
||||
sys_exit(1)
|
||||
if dirty:
|
||||
keypair.save()
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument("--certificate", type=str, required=True)
|
||||
parser.add_argument("--private-key", type=str, required=False)
|
||||
parser.add_argument("--name", type=str, required=True)
|
@ -6,7 +6,12 @@ from uuid import uuid4
|
||||
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
from cryptography.hazmat.primitives.asymmetric.types import PRIVATE_KEY_TYPES, PUBLIC_KEY_TYPES
|
||||
from cryptography.hazmat.primitives.asymmetric.ec import (
|
||||
EllipticCurvePrivateKey,
|
||||
EllipticCurvePublicKey,
|
||||
)
|
||||
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey, Ed25519PublicKey
|
||||
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey, RSAPublicKey
|
||||
from cryptography.hazmat.primitives.serialization import load_pem_private_key
|
||||
from cryptography.x509 import Certificate, load_pem_x509_certificate
|
||||
from django.db import models
|
||||
@ -37,8 +42,8 @@ class CertificateKeyPair(SerializerModel, ManagedModel, CreatedUpdatedModel):
|
||||
)
|
||||
|
||||
_cert: Optional[Certificate] = None
|
||||
_private_key: Optional[PRIVATE_KEY_TYPES] = None
|
||||
_public_key: Optional[PUBLIC_KEY_TYPES] = None
|
||||
_private_key: Optional[RSAPrivateKey | EllipticCurvePrivateKey | Ed25519PrivateKey] = None
|
||||
_public_key: Optional[RSAPublicKey | EllipticCurvePublicKey | Ed25519PublicKey] = None
|
||||
|
||||
@property
|
||||
def serializer(self) -> Serializer:
|
||||
@ -56,7 +61,7 @@ class CertificateKeyPair(SerializerModel, ManagedModel, CreatedUpdatedModel):
|
||||
return self._cert
|
||||
|
||||
@property
|
||||
def public_key(self) -> Optional[PUBLIC_KEY_TYPES]:
|
||||
def public_key(self) -> Optional[RSAPublicKey | EllipticCurvePublicKey | Ed25519PublicKey]:
|
||||
"""Get public key of the private key"""
|
||||
if not self._public_key:
|
||||
self._public_key = self.private_key.public_key()
|
||||
@ -65,7 +70,7 @@ class CertificateKeyPair(SerializerModel, ManagedModel, CreatedUpdatedModel):
|
||||
@property
|
||||
def private_key(
|
||||
self,
|
||||
) -> Optional[PRIVATE_KEY_TYPES]:
|
||||
) -> Optional[RSAPrivateKey | EllipticCurvePrivateKey | Ed25519PrivateKey]:
|
||||
"""Get python cryptography PrivateKey instance"""
|
||||
if not self._private_key and self.key_data != "":
|
||||
try:
|
||||
|
@ -1,6 +1,5 @@
|
||||
"""Crypto tests"""
|
||||
import datetime
|
||||
from json import loads
|
||||
from os import makedirs
|
||||
from tempfile import TemporaryDirectory
|
||||
|
||||
@ -87,35 +86,13 @@ class TestCrypto(APITestCase):
|
||||
|
||||
def test_list(self):
|
||||
"""Test API List"""
|
||||
cert = create_test_cert()
|
||||
self.client.force_login(create_test_admin_user())
|
||||
response = self.client.get(
|
||||
reverse(
|
||||
"authentik_api:certificatekeypair-list",
|
||||
)
|
||||
+ f"?name={cert.name}"
|
||||
)
|
||||
self.assertEqual(200, response.status_code)
|
||||
body = loads(response.content.decode())
|
||||
api_cert = [x for x in body["results"] if x["name"] == cert.name][0]
|
||||
self.assertEqual(api_cert["fingerprint_sha1"], cert.fingerprint_sha1)
|
||||
self.assertEqual(api_cert["fingerprint_sha256"], cert.fingerprint_sha256)
|
||||
|
||||
def test_list_without_details(self):
|
||||
"""Test API List (no details)"""
|
||||
cert = create_test_cert()
|
||||
self.client.force_login(create_test_admin_user())
|
||||
response = self.client.get(
|
||||
reverse(
|
||||
"authentik_api:certificatekeypair-list",
|
||||
)
|
||||
+ f"?name={cert.name}&include_details=false"
|
||||
)
|
||||
self.assertEqual(200, response.status_code)
|
||||
body = loads(response.content.decode())
|
||||
api_cert = [x for x in body["results"] if x["name"] == cert.name][0]
|
||||
self.assertEqual(api_cert["fingerprint_sha1"], None)
|
||||
self.assertEqual(api_cert["fingerprint_sha256"], None)
|
||||
|
||||
def test_certificate_download(self):
|
||||
"""Test certificate export (download)"""
|
||||
|
@ -85,18 +85,16 @@ class NotificationTransportViewSet(UsedByMixin, ModelViewSet):
|
||||
"""Send example notification using selected transport. Requires
|
||||
Modify permissions."""
|
||||
transport: NotificationTransport = self.get_object()
|
||||
event = Event.new(
|
||||
action="notification_test",
|
||||
user=get_user(request.user),
|
||||
app=self.__class__.__module__,
|
||||
context={"foo": "bar"},
|
||||
)
|
||||
event.save()
|
||||
notification = Notification(
|
||||
severity=NotificationSeverity.NOTICE,
|
||||
body=f"Test Notification from transport {transport.name}",
|
||||
user=request.user,
|
||||
event=event,
|
||||
event=Event(
|
||||
action="Test",
|
||||
user=get_user(request.user),
|
||||
app=self.__class__.__module__,
|
||||
context={"foo": "bar"},
|
||||
),
|
||||
)
|
||||
try:
|
||||
response = NotificationTransportTestSerializer(
|
||||
|
@ -1,7 +1,7 @@
|
||||
"""authentik events app"""
|
||||
from prometheus_client import Gauge
|
||||
|
||||
from authentik.blueprints.apps import ManagedAppConfig
|
||||
from authentik.blueprints.manager import ManagedAppConfig
|
||||
|
||||
GAUGE_TASKS = Gauge(
|
||||
"authentik_system_tasks",
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user