Compare commits

..

3 Commits

Author SHA1 Message Date
10b1bd5bf4 release: 2022.9.1 2022-11-02 21:54:21 +01:00
55cc1812c0 core: bump golang from 1.19.2-bullseye to 1.19.3-bullseye (#3925)
Bumps golang from 1.19.2-bullseye to 1.19.3-bullseye.

---
updated-dependencies:
- dependency-name: golang
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-11-02 10:09:00 +01:00
6962d8c59c core: bump golang from 1.19.1-bullseye to 1.19.2-bullseye (#3728) 2022-11-02 10:08:54 +01:00
518 changed files with 12803 additions and 32778 deletions

View File

@ -1,5 +1,5 @@
[bumpversion] [bumpversion]
current_version = 2022.10.2 current_version = 2022.9.1
tag = True tag = True
commit = True commit = True
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+) parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)

View File

@ -32,27 +32,32 @@ runs:
shell: python shell: python
run: | run: |
"""Helper script to get the actual branch name, docker safe""" """Helper script to get the actual branch name, docker safe"""
import configparser
import os import os
from time import time from time import time
parser = configparser.ConfigParser() env_pr_branch = "GITHUB_HEAD_REF"
parser.read(".bumpversion.cfg") default_branch = "GITHUB_REF"
sha = "GITHUB_SHA"
branch_name = os.environ["GITHUB_REF"] branch_name = os.environ[default_branch]
if os.environ.get("GITHUB_HEAD_REF", "") != "": if os.environ.get(env_pr_branch, "") != "":
branch_name = os.environ["GITHUB_HEAD_REF"] branch_name = os.environ[env_pr_branch]
should_build = str(os.environ.get("DOCKER_USERNAME", "") != "").lower() 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 = parser.get("bumpversion", "current_version")
version_family = ".".join(version.split(".")[:-1]) version_family = ".".join(version.split(".")[:-1])
safe_branch_name = branch_name.replace("refs/heads/", "").replace("/", "-") print("##[set-output name=version]%s" % version)
print("##[set-output name=versionFamily]%s" % version_family)
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)

View File

@ -1,5 +1,5 @@
name: 'Setup authentik testing environment' name: 'Setup authentik testing environemnt'
description: 'Setup authentik testing environment' description: 'Setup authentik testing environemnt'
runs: runs:
using: "composite" using: "composite"

16
.github/transifex.yml vendored
View File

@ -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'

View File

@ -108,7 +108,7 @@ jobs:
with: with:
domain: ${{github.repository_owner}} domain: ${{github.repository_owner}}
- name: Create k8s Kind Cluster - name: Create k8s Kind Cluster
uses: helm/kind-action@v1.4.0 uses: helm/kind-action@v1.3.0
- name: run integration - name: run integration
run: | run: |
poetry run make test-integration poetry run make test-integration
@ -130,7 +130,7 @@ jobs:
- uses: testspace-com/setup-testspace@v1 - uses: testspace-com/setup-testspace@v1
with: with:
domain: ${{github.repository_owner}} domain: ${{github.repository_owner}}
- name: Setup e2e env (chrome, etc) - name: Setup authentik env
run: | run: |
docker-compose -f tests/e2e/docker-compose.yml up -d docker-compose -f tests/e2e/docker-compose.yml up -d
- id: cache-web - id: cache-web
@ -166,7 +166,7 @@ jobs:
- uses: testspace-com/setup-testspace@v1 - uses: testspace-com/setup-testspace@v1
with: with:
domain: ${{github.repository_owner}} domain: ${{github.repository_owner}}
- name: Setup e2e env (chrome, etc) - name: Setup authentik env
run: | run: |
docker-compose -f tests/e2e/docker-compose.yml up -d docker-compose -f tests/e2e/docker-compose.yml up -d
- id: cache-web - id: cache-web
@ -217,7 +217,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: Set up QEMU - 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 - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v2
- name: prepare variables - name: prepare variables

View File

@ -63,7 +63,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: Set up QEMU - 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 - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v2
- name: prepare variables - name: prepare variables
@ -111,7 +111,7 @@ jobs:
- uses: actions/setup-go@v3 - uses: actions/setup-go@v3
with: with:
go-version: "^1.17" go-version: "^1.17"
- uses: actions/setup-node@v3.5.1 - uses: actions/setup-node@v3.4.1
with: with:
node-version: '16' node-version: '16'
cache: 'npm' cache: 'npm'

View File

@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-node@v3.5.1 - uses: actions/setup-node@v3.4.1
with: with:
node-version: '16' node-version: '16'
cache: 'npm' cache: 'npm'
@ -31,7 +31,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-node@v3.5.1 - uses: actions/setup-node@v3.4.1
with: with:
node-version: '16' node-version: '16'
cache: 'npm' cache: 'npm'
@ -47,7 +47,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-node@v3.5.1 - uses: actions/setup-node@v3.4.1
with: with:
node-version: '16' node-version: '16'
cache: 'npm' cache: 'npm'
@ -78,7 +78,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-node@v3.5.1 - uses: actions/setup-node@v3.4.1
with: with:
node-version: '16' node-version: '16'
cache: 'npm' cache: 'npm'

View File

@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-node@v3.5.1 - uses: actions/setup-node@v3.4.1
with: with:
node-version: '16' node-version: '16'
cache: 'npm' cache: 'npm'

View File

@ -10,7 +10,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: Set up QEMU - 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 - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v2
- name: prepare variables - name: prepare variables
@ -54,7 +54,7 @@ jobs:
with: with:
go-version: "^1.17" go-version: "^1.17"
- name: Set up QEMU - 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 - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v2
- name: prepare variables - name: prepare variables
@ -100,7 +100,7 @@ jobs:
- uses: actions/setup-go@v3 - uses: actions/setup-go@v3
with: with:
go-version: "^1.17" go-version: "^1.17"
- uses: actions/setup-node@v3.5.1 - uses: actions/setup-node@v3.4.1
with: with:
node-version: '16' node-version: '16'
cache: 'npm' cache: 'npm'

View File

@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-node@v3.5.1 - uses: actions/setup-node@v3.4.1
with: with:
node-version: '16' node-version: '16'
registry-url: 'https://registry.npmjs.org' registry-url: 'https://registry.npmjs.org'

View File

@ -17,24 +17,24 @@ diverse, inclusive, and healthy community.
Examples of behavior that contributes to a positive environment for our Examples of behavior that contributes to a positive environment for our
community include: community include:
- Demonstrating empathy and kindness toward other people * Demonstrating empathy and kindness toward other people
- Being respectful of differing opinions, viewpoints, and experiences * Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback * Giving and gracefully accepting constructive feedback
- Accepting responsibility and apologizing to those affected by our mistakes, * Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience and learning from the experience
- Focusing on what is best not just for us as individuals, but for the * Focusing on what is best not just for us as individuals, but for the
overall community overall community
Examples of unacceptable behavior include: Examples of unacceptable behavior include:
- The use of sexualized language or imagery, and sexual attention or * The use of sexualized language or imagery, and sexual attention or
advances of any kind advances of any kind
- Trolling, insulting or derogatory comments, and personal or political attacks * Trolling, insulting or derogatory comments, and personal or political attacks
- Public or private harassment * Public or private harassment
- Publishing others' private information, such as a physical or email * Publishing others' private information, such as a physical or email
address, without their explicit permission address, without their explicit permission
- Other conduct which could reasonably be considered inappropriate in a * Other conduct which could reasonably be considered inappropriate in a
professional setting professional setting
## Enforcement Responsibilities ## Enforcement Responsibilities
@ -106,7 +106,7 @@ Violating these terms may lead to a permanent ban.
### 4. Permanent Ban ### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community **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. individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within **Consequence**: A permanent ban from any sort of public interaction within

View File

@ -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) [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) [What should I know before I get started?](#what-should-i-know-before-i-get-started)
* [The components](#the-components)
- [The components](#the-components) * [authentik's structure](#authentiks-structure)
- [authentik's structure](#authentiks-structure)
[How Can I Contribute?](#how-can-i-contribute) [How Can I Contribute?](#how-can-i-contribute)
* [Reporting Bugs](#reporting-bugs)
- [Reporting Bugs](#reporting-bugs) * [Suggesting Enhancements](#suggesting-enhancements)
- [Suggesting Enhancements](#suggesting-enhancements) * [Your First Code Contribution](#your-first-code-contribution)
- [Your First Code Contribution](#your-first-code-contribution) * [Pull Requests](#pull-requests)
- [Pull Requests](#pull-requests)
[Styleguides](#styleguides) [Styleguides](#styleguides)
* [Git Commit Messages](#git-commit-messages)
- [Git Commit Messages](#git-commit-messages) * [Python Styleguide](#python-styleguide)
- [Python Styleguide](#python-styleguide) * [Documentation Styleguide](#documentation-styleguide)
- [Documentation Styleguide](#documentation-styleguide)
## Code of Conduct ## 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 consists of a few larger components:
- _authentik_ the actual application server, is described below. - *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-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 - *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. - *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. - *website* is the Website/documentation, which uses docusaurus.
### authentik's structure ### 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: The process described here has several goals:
- Maintain authentik's quality - Maintain authentik's quality
- Fix problems that are important to users - Fix problems that are important to users
- Engage the community in working toward the best possible authentik - Engage the community in working toward the best possible authentik
- Enable a sustainable system for authentik's maintainers to review contributions - Enable a sustainable system for authentik's maintainers to review contributions
Please follow these steps to have your contribution considered by the maintainers: 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 ### Git Commit Messages
- Use the format of `<package>: <verb> <description>` * Use the format of `<package>: <verb> <description>`
- See [here](#authentik-packages) for `package` - See [here](#authentik-packages) for `package`
- Example: `providers/saml2: fix parsing of requests` - Example: `providers/saml2: fix parsing of requests`
- Reference issues and pull requests liberally after the first line * Reference issues and pull requests liberally after the first line
### Python Styleguide ### 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. authentik runs on Python 3.9 at the time of writing this.
- Use native type-annotations wherever possible. * Use native type-annotations wherever possible.
- Add meaningful docstrings when possible. * Add meaningful docstrings when possible.
- Ensure any database migrations work properly from the last stable version (this is checked via CI) * 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. * If your code changes central functions, make sure nothing else is broken.
### Documentation Styleguide ### Documentation Styleguide
- Use [MDX](https://mdxjs.com/) whenever appropriate. * Use [MDX](https://mdxjs.com/) whenever appropriate.

View File

@ -3,7 +3,6 @@ FROM --platform=${BUILDPLATFORM} docker.io/node:18 as website-builder
COPY ./website /work/website/ COPY ./website /work/website/
COPY ./blueprints /work/blueprints/ COPY ./blueprints /work/blueprints/
COPY ./SECURITY.md /work/
ENV NODE_ENV=production ENV NODE_ENV=production
WORKDIR /work/website WORKDIR /work/website
@ -31,7 +30,7 @@ RUN pip install --no-cache-dir poetry && \
poetry export -f requirements.txt --dev --output requirements-dev.txt poetry export -f requirements.txt --dev --output requirements-dev.txt
# Stage 4: Build go proxy # Stage 4: Build go proxy
FROM docker.io/golang:1.19.2-bullseye AS go-builder FROM docker.io/golang:1.19.3-bullseye AS go-builder
WORKDIR /work WORKDIR /work
@ -44,7 +43,7 @@ COPY ./internal /work/internal
COPY ./go.mod /work/go.mod COPY ./go.mod /work/go.mod
COPY ./go.sum /work/go.sum 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 # Stage 5: Run
FROM docker.io/python:3.10.7-slim-bullseye AS final-image FROM docker.io/python:3.10.7-slim-bullseye AS final-image

View File

@ -53,7 +53,7 @@ migrate:
python -m lifecycle.migrate python -m lifecycle.migrate
run: run:
go run -v ./cmd/server/ go run -v cmd/server/main.go
i18n-extract: i18n-extract-core web-extract i18n-extract: i18n-extract-core web-extract
@ -73,7 +73,7 @@ gen-diff:
docker run \ docker run \
--rm -v ${PWD}:/local \ --rm -v ${PWD}:/local \
--user ${UID}:${GID} \ --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 \ --markdown /local/diff.md \
/local/old_schema.yml /local/schema.yml /local/old_schema.yml /local/schema.yml
rm old_schema.yml rm old_schema.yml
@ -90,11 +90,11 @@ gen-client-ts:
-i /local/schema.yml \ -i /local/schema.yml \
-g typescript-fetch \ -g typescript-fetch \
-o /local/gen-ts-api \ -o /local/gen-ts-api \
-c /local/scripts/api-ts-config.yaml \ --additional-properties=typescriptThreePlus=true,supportsES6=true,npmName=@goauthentik/api,npmVersion=${NPM_VERSION} \
--additional-properties=npmVersion=${NPM_VERSION} \
--git-repo-id authentik \ --git-repo-id authentik \
--git-user-id goauthentik --git-user-id goauthentik
mkdir -p web/node_modules/@goauthentik/api 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 cd gen-ts-api && npm i
\cp -rfv gen-ts-api/* web/node_modules/@goauthentik/api \cp -rfv gen-ts-api/* web/node_modules/@goauthentik/api
@ -151,7 +151,7 @@ web-extract:
## Website ## Website
######################### #########################
website: website-lint-fix website-build website: website-lint-fix
website-install: website-install:
cd website && npm ci cd website && npm ci
@ -159,9 +159,6 @@ website-install:
website-lint-fix: website-lint-fix:
cd website && npm run prettier cd website && npm run prettier
website-build:
cd website && npm run build
website-watch: website-watch:
cd website && npm run watch cd website && npm run watch

View File

@ -26,10 +26,10 @@ For bigger setups, there is a Helm Chart [here](https://github.com/goauthentik/h
## Screenshots ## Screenshots
| Light | Dark | Light | Dark
| ------------------------------------------------------ | ----------------------------------------------------- | --- | ---
| ![](https://goauthentik.io/img/screen_apps_light.jpg) | ![](https://goauthentik.io/img/screen_apps_dark.jpg) | ![](https://goauthentik.io/img/screen_apps_light.jpg) | ![](https://goauthentik.io/img/screen_apps_dark.jpg)
| ![](https://goauthentik.io/img/screen_admin_light.jpg) | ![](https://goauthentik.io/img/screen_admin_dark.jpg) | ![](https://goauthentik.io/img/screen_admin_light.jpg) | ![](https://goauthentik.io/img/screen_admin_dark.jpg)
## Development ## Development

View File

@ -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 ## Supported Versions
(.x being the latest patch release for each version) (.x being the latest patch release for each version)
| Version | Supported | | Version | Supported |
| --------- | ------------------ | | ---------- | ------------------ |
| 2022.10.x | :white_check_mark: | | 2022.6.x | :white_check_mark: |
| 2022.11.x | :white_check_mark: | | 2022.7.x | :white_check_mark: |
| 2022.8.x | :white_check_mark: |
## Reporting a Vulnerability ## 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. To report a vulnerability, send an email to [security@goauthentik.io](mailto:security@goauthentik.io)
## 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.

View File

@ -2,7 +2,7 @@
from os import environ from os import environ
from typing import Optional from typing import Optional
__version__ = "2022.10.2" __version__ = "2022.9.1"
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH" ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"

View File

@ -23,7 +23,6 @@ class LoginMetricsSerializer(PassiveSerializer):
logins_per_1h = SerializerMethodField() logins_per_1h = SerializerMethodField()
logins_failed_per_1h = SerializerMethodField() logins_failed_per_1h = SerializerMethodField()
authorizations_per_1h = SerializerMethodField()
@extend_schema_field(CoordinateSerializer(many=True)) @extend_schema_field(CoordinateSerializer(many=True))
def get_logins_per_1h(self, _): def get_logins_per_1h(self, _):
@ -45,16 +44,6 @@ class LoginMetricsSerializer(PassiveSerializer):
.get_events_per_hour() .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): class AdministrationMetricsViewSet(APIView):
"""Login Metrics per 1h""" """Login Metrics per 1h"""

View File

@ -28,7 +28,6 @@ class Capabilities(models.TextChoices):
CAN_SAVE_MEDIA = "can_save_media" CAN_SAVE_MEDIA = "can_save_media"
CAN_GEO_IP = "can_geo_ip" CAN_GEO_IP = "can_geo_ip"
CAN_IMPERSONATE = "can_impersonate" CAN_IMPERSONATE = "can_impersonate"
CAN_DEBUG = "can_debug"
class ErrorReportingConfigSerializer(PassiveSerializer): class ErrorReportingConfigSerializer(PassiveSerializer):
@ -67,8 +66,6 @@ class ConfigView(APIView):
caps.append(Capabilities.CAN_GEO_IP) caps.append(Capabilities.CAN_GEO_IP)
if CONFIG.y_bool("impersonation"): if CONFIG.y_bool("impersonation"):
caps.append(Capabilities.CAN_IMPERSONATE) caps.append(Capabilities.CAN_IMPERSONATE)
if settings.DEBUG:
caps.append(Capabilities.CAN_DEBUG)
return caps return caps
def get_config(self) -> ConfigSerializer: def get_config(self) -> ConfigSerializer:

View File

@ -59,8 +59,7 @@ from authentik.sources.oauth.api.source import OAuthSourceViewSet
from authentik.sources.oauth.api.source_connection import UserOAuthSourceConnectionViewSet from authentik.sources.oauth.api.source_connection import UserOAuthSourceConnectionViewSet
from authentik.sources.plex.api.source import PlexSourceViewSet from authentik.sources.plex.api.source import PlexSourceViewSet
from authentik.sources.plex.api.source_connection import PlexSourceConnectionViewSet from authentik.sources.plex.api.source_connection import PlexSourceConnectionViewSet
from authentik.sources.saml.api.source import SAMLSourceViewSet from authentik.sources.saml.api import SAMLSourceViewSet
from authentik.sources.saml.api.source_connection import UserSAMLSourceConnectionViewSet
from authentik.stages.authenticator_duo.api import ( from authentik.stages.authenticator_duo.api import (
AuthenticatorDuoStageViewSet, AuthenticatorDuoStageViewSet,
DuoAdminDeviceViewSet, DuoAdminDeviceViewSet,
@ -139,7 +138,6 @@ router.register("sources/all", SourceViewSet)
router.register("sources/user_connections/all", UserSourceConnectionViewSet) router.register("sources/user_connections/all", UserSourceConnectionViewSet)
router.register("sources/user_connections/oauth", UserOAuthSourceConnectionViewSet) router.register("sources/user_connections/oauth", UserOAuthSourceConnectionViewSet)
router.register("sources/user_connections/plex", PlexSourceConnectionViewSet) router.register("sources/user_connections/plex", PlexSourceConnectionViewSet)
router.register("sources/user_connections/saml", UserSAMLSourceConnectionViewSet)
router.register("sources/ldap", LDAPSourceViewSet) router.register("sources/ldap", LDAPSourceViewSet)
router.register("sources/saml", SAMLSourceViewSet) router.register("sources/saml", SAMLSourceViewSet)
router.register("sources/oauth", OAuthSourceViewSet) router.register("sources/oauth", OAuthSourceViewSet)

View File

@ -63,7 +63,7 @@ class BlueprintEntry:
all_attrs = get_attrs(model) all_attrs = get_attrs(model)
for extra_identifier_name in extra_identifier_names: 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( return BlueprintEntry(
identifiers=identifiers, identifiers=identifiers,
model=f"{model._meta.app_label}.{model._meta.model_name}", model=f"{model._meta.app_label}.{model._meta.model_name}",
@ -139,7 +139,7 @@ class KeyOf(YAMLTag):
): ):
return _entry._state.instance.pbm_uuid return _entry._state.instance.pbm_uuid
return _entry._state.instance.pk 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" 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(UUID, lambda self, data: self.represent_str(str(data)))
self.add_representer(OrderedDict, lambda self, data: self.represent_dict(dict(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(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: def represent(self, data) -> None:
if is_dataclass(data): if is_dataclass(data):

View File

@ -187,10 +187,7 @@ class Importer:
if "pk" in updated_identifiers: if "pk" in updated_identifiers:
model_instance.pk = updated_identifiers["pk"] model_instance.pk = updated_identifiers["pk"]
serializer_kwargs["instance"] = model_instance serializer_kwargs["instance"] = model_instance
try: full_data = self.__update_pks_for_attrs(entry.get_attrs(self.__import))
full_data = self.__update_pks_for_attrs(entry.get_attrs(self.__import))
except ValueError as exc:
raise EntryInvalidError(exc) from exc
full_data.update(updated_identifiers) full_data.update(updated_identifiers)
serializer_kwargs["data"] = full_data serializer_kwargs["data"] = full_data

View File

@ -232,11 +232,7 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet):
return Response({}) return Response({})
if icon: if icon:
app.meta_icon = icon app.meta_icon = icon
try: app.save()
app.save()
except PermissionError as exc:
LOGGER.warning("Failed to save icon", exc=exc)
return HttpResponseBadRequest()
return Response({}) return Response({})
return HttpResponseBadRequest() return HttpResponseBadRequest()

View File

@ -470,7 +470,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
# pylint: disable=invalid-name, unused-argument # pylint: disable=invalid-name, unused-argument
def recovery_email(self, request: Request, pk: int) -> Response: def recovery_email(self, request: Request, pk: int) -> Response:
"""Create a temporary link that a user can use to recover their accounts""" """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 == "": if for_user.email == "":
LOGGER.debug("User doesn't have an email address") LOGGER.debug("User doesn't have an email address")
return Response(status=404) return Response(status=404)
@ -488,9 +488,8 @@ class UserViewSet(UsedByMixin, ModelViewSet):
email_stage: EmailStage = stages.first() email_stage: EmailStage = stages.first()
message = TemplateEmailMessage( message = TemplateEmailMessage(
subject=_(email_stage.subject), subject=_(email_stage.subject),
to=[for_user.email],
template_name=email_stage.template, template_name=email_stage.template,
language=for_user.locale(request), to=[for_user.email],
template_context={ template_context={
"url": link, "url": link,
"user": for_user, "user": for_user,

View File

@ -4,6 +4,7 @@ from typing import Optional
from django.db.models import Model from django.db.models import Model
from django.http import HttpRequest from django.http import HttpRequest
from guardian.utils import get_anonymous_user
from authentik.core.models import User from authentik.core.models import User
from authentik.events.models import Event, EventAction from authentik.events.models import Event, EventAction
@ -26,7 +27,7 @@ class PropertyMappingEvaluator(BaseEvaluator):
else: else:
_filename = str(model) _filename = str(model)
super().__init__(filename=_filename) super().__init__(filename=_filename)
req = PolicyRequest(user=User()) req = PolicyRequest(user=get_anonymous_user())
req.obj = model req.obj = model
if user: if user:
req.user = user req.user = user

View File

@ -4,7 +4,6 @@ from typing import Callable, Optional
from uuid import uuid4 from uuid import uuid4
from django.http import HttpRequest, HttpResponse from django.http import HttpRequest, HttpResponse
from django.utils.translation import activate
from sentry_sdk.api import set_tag from sentry_sdk.api import set_tag
from structlog.contextvars import STRUCTLOG_KEY_PREFIX from structlog.contextvars import STRUCTLOG_KEY_PREFIX
@ -30,10 +29,6 @@ class ImpersonateMiddleware:
def __call__(self, request: HttpRequest) -> HttpResponse: def __call__(self, request: HttpRequest) -> HttpResponse:
# No permission checks are done here, they need to be checked before # No permission checks are done here, they need to be checked before
# SESSION_KEY_IMPERSONATE_USER is set. # 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: if SESSION_KEY_IMPERSONATE_USER in request.session:
request.user = request.session[SESSION_KEY_IMPERSONATE_USER] request.user = request.session[SESSION_KEY_IMPERSONATE_USER]

View 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",
),
),
]

View 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)
),
]

View 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",
},
),
]

View 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",
),
),
]

View 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),
),
]

View 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"),
),
]

View 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"),
),
]

View 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()),
],
),
]

View 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",
},
),
]

View 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,
),
]

View 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",
),
]

View 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")},
),
]

View 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),
]

View 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/"),
),
]

View 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"),
),
]

View 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",
},
),
]

View 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",
),
),
]

View 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.",
),
),
]

View 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
),
),
]

View 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),
]

View File

@ -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()],
),
),
]

View File

@ -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"),
),
]

View 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),
),
]

View File

@ -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/"),
),
]

View File

@ -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",
},
),
]

View 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),
]

View 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",
),
),
]

View File

@ -220,17 +220,6 @@ class User(SerializerModel, GuardianUserMixin, AbstractUser):
"""Generate a globally unique UID, based on the user ID and the hashed secret key""" """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() 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 @property
def avatar(self) -> str: def avatar(self) -> str:
"""Get avatar, depending on authentik.avatar setting""" """Get avatar, depending on authentik.avatar setting"""
@ -483,21 +472,6 @@ class Source(ManagedModel, SerializerModel, PolicyBindingModel):
def __str__(self): def __str__(self):
return self.name return self.name
class Meta:
indexes = [
models.Index(
fields=[
"slug",
]
),
models.Index(
fields=[
"name",
]
),
]
class UserSourceConnection(SerializerModel, CreatedUpdatedModel): class UserSourceConnection(SerializerModel, CreatedUpdatedModel):
"""Connection between User and Source.""" """Connection between User and Source."""

View File

@ -5,7 +5,7 @@ from typing import Any, Optional
from django.contrib import messages from django.contrib import messages
from django.db import IntegrityError from django.db import IntegrityError
from django.db.models.query_utils import Q 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.shortcuts import redirect
from django.urls import reverse from django.urls import reverse
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
@ -23,10 +23,8 @@ from authentik.flows.planner import (
PLAN_CONTEXT_SSO, PLAN_CONTEXT_SSO,
FlowPlanner, FlowPlanner,
) )
from authentik.flows.stage import StageView
from authentik.flows.views.executor import NEXT_ARG_NAME, SESSION_KEY_GET, SESSION_KEY_PLAN 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.utils.urls import redirect_with_qs
from authentik.lib.views import bad_request_message
from authentik.policies.denied import AccessDeniedResponse from authentik.policies.denied import AccessDeniedResponse
from authentik.policies.utils import delete_none_keys from authentik.policies.utils import delete_none_keys
from authentik.stages.password import BACKEND_INBUILT from authentik.stages.password import BACKEND_INBUILT
@ -45,26 +43,6 @@ class Action(Enum):
DENY = "deny" 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: class SourceFlowManager:
"""Help sources decide what they should do after authorization. Based on source settings and """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 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) action, connection = self.get_action(**kwargs)
except IntegrityError as exc: except IntegrityError as exc:
self._logger.warning("failed to get action", exc=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) self._logger.debug("get_action", action=action, connection=connection)
try: try:
if connection: if connection:
if action == Action.LINK: if action == Action.LINK:
self._logger.debug("Linking existing user") self._logger.debug("Linking existing user")
return self.handle_existing_link(connection) return self.handle_existing_user_link(connection)
if action == Action.AUTH: if action == Action.AUTH:
self._logger.debug("Handling auth user") self._logger.debug("Handling auth user")
return self.handle_auth(connection) return self.handle_auth_user(connection)
if action == Action.ENROLL: if action == Action.ENROLL:
self._logger.debug("Handling enrollment of new user") self._logger.debug("Handling enrollment of new user")
return self.handle_enroll(connection) return self.handle_enroll(connection)
@ -220,12 +198,8 @@ class SourceFlowManager:
] ]
return [] return []
def _prepare_flow( def _handle_login_flow(
self, self, flow: Flow, connection: UserSourceConnection, **kwargs
flow: Flow,
connection: UserSourceConnection,
stages: Optional[list[StageView]] = None,
**kwargs,
) -> HttpResponse: ) -> HttpResponse:
"""Prepare Authentication Plan, redirect user FlowExecutor""" """Prepare Authentication Plan, redirect user FlowExecutor"""
# Ensure redirect is carried through when user was trying to # Ensure redirect is carried through when user was trying to
@ -245,18 +219,12 @@ class SourceFlowManager:
) )
kwargs.update(self.policy_context) kwargs.update(self.policy_context)
if not flow: if not flow:
return bad_request_message( return HttpResponseBadRequest()
self.request,
_("Configured flow does not exist."),
)
# We run the Flow planner here so we can pass the Pending user in the context # We run the Flow planner here so we can pass the Pending user in the context
planner = FlowPlanner(flow) planner = FlowPlanner(flow)
plan = planner.plan(self.request, kwargs) plan = planner.plan(self.request, kwargs)
for stage in self.get_stages_to_append(flow): for stage in self.get_stages_to_append(flow):
plan.append_stage(stage) plan.append_stage(stage=stage)
if stages:
for stage in stages:
plan.append_stage(stage)
self.request.session[SESSION_KEY_PLAN] = plan self.request.session[SESSION_KEY_PLAN] = plan
return redirect_with_qs( return redirect_with_qs(
"authentik_core:if-flow", "authentik_core:if-flow",
@ -265,35 +233,24 @@ class SourceFlowManager:
) )
# pylint: disable=unused-argument # pylint: disable=unused-argument
def handle_auth( def handle_auth_user(
self, self,
connection: UserSourceConnection, connection: UserSourceConnection,
) -> HttpResponse: ) -> HttpResponse:
"""Login user and redirect.""" """Login user and redirect."""
flow_kwargs = {PLAN_CONTEXT_PENDING_USER: connection.user} messages.success(
return self._prepare_flow( self.request,
self.source.authentication_flow, _("Successfully authenticated with %(source)s!" % {"source": self.source.name}),
connection,
stages=[
in_memory_stage(
MessageStage,
message=_(
"Successfully authenticated with %(source)s!" % {"source": self.source.name}
),
)
],
**flow_kwargs,
) )
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, self,
connection: UserSourceConnection, connection: UserSourceConnection,
) -> HttpResponse: ) -> HttpResponse:
"""Handler when the user was already authenticated and linked an external source """Handler when the user was already authenticated and linked an external source
to their account.""" 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 # Connection has already been saved
Event.new( Event.new(
EventAction.SOURCE_LINKED, EventAction.SOURCE_LINKED,
@ -304,6 +261,9 @@ class SourceFlowManager:
self.request, self.request,
_("Successfully linked %(source)s!" % {"source": self.source.name}), _("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( return redirect(
reverse( reverse(
"authentik_core:if-user", "authentik_core:if-user",
@ -316,24 +276,18 @@ class SourceFlowManager:
connection: UserSourceConnection, connection: UserSourceConnection,
) -> HttpResponse: ) -> HttpResponse:
"""User was not authenticated and previous request was not authenticated.""" """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 # We run the Flow planner here so we can pass the Pending user in the context
if not self.source.enrollment_flow: if not self.source.enrollment_flow:
self._logger.warning("source has no enrollment flow") self._logger.warning("source has no enrollment flow")
return bad_request_message( return HttpResponseBadRequest()
self.request, return self._handle_login_flow(
_("Source is not configured for enrollment."),
)
return self._prepare_flow(
self.source.enrollment_flow, self.source.enrollment_flow,
connection, 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_PROMPT: delete_none_keys(self.enroll_info),
PLAN_CONTEXT_USER_PATH: self.source.get_user_path(), PLAN_CONTEXT_USER_PATH: self.source.get_user_path(),

View File

@ -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>

View File

@ -9,12 +9,17 @@
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)"> <meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">
<link rel="icon" href="{{ tenant.branding_favicon }}"> <link rel="icon" href="{{ tenant.branding_favicon }}">
<link rel="shortcut 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 %} {% endblock %}
{% block body %} {% block body %}
<ak-message-container></ak-message-container> <ak-message-container data-refresh-on-locale="true"></ak-message-container>
<ak-interface-admin> <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"> <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" style="height: 100vh;">
<div class="pf-c-empty-state__content"> <div class="pf-c-empty-state__content">

View File

@ -4,7 +4,7 @@
{% load i18n %} {% load i18n %}
{% block title %} {% block title %}
{{ tenant.branding_title }} {% trans 'End session' %} - {{ tenant.branding_title }}
{% endblock %} {% endblock %}
{% block card_title %} {% block card_title %}

View File

@ -11,8 +11,11 @@
{% if flow.compatibility_mode and not inspector %} {% if flow.compatibility_mode and not inspector %}
<script>ShadyDOM = { force: !navigator.webdriver };</script> <script>ShadyDOM = { force: !navigator.webdriver };</script>
{% endif %} {% endif %}
{% include "base/header_js.html" %}
<script> <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 = { window.authentik.flow = {
"layout": "{{ flow.layout }}", "layout": "{{ flow.layout }}",
}; };
@ -29,8 +32,8 @@ window.authentik.flow = {
{% endblock %} {% endblock %}
{% block body %} {% block body %}
<ak-message-container></ak-message-container> <ak-message-container data-refresh-on-locale="true"></ak-message-container>
<ak-flow-executor> <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"> <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" style="height: 100vh;">
<div class="pf-c-empty-state__content"> <div class="pf-c-empty-state__content">

View File

@ -9,12 +9,17 @@
<meta name="theme-color" content="#151515" media="(prefers-color-scheme: dark)"> <meta name="theme-color" content="#151515" media="(prefers-color-scheme: dark)">
<link rel="icon" href="{{ tenant.branding_favicon }}"> <link rel="icon" href="{{ tenant.branding_favicon }}">
<link rel="shortcut 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 %} {% endblock %}
{% block body %} {% block body %}
<ak-message-container></ak-message-container> <ak-message-container data-refresh-on-locale="true"></ak-message-container>
<ak-interface-user> <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"> <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" style="height: 100vh;">
<div class="pf-c-empty-state__content"> <div class="pf-c-empty-state__content">

View File

@ -6,7 +6,6 @@
{% block head_before %} {% block head_before %}
<link rel="prefetch" href="/static/dist/assets/images/flow_background.jpg" /> <link rel="prefetch" href="/static/dist/assets/images/flow_background.jpg" />
<link rel="stylesheet" type="text/css" href="{% static 'dist/patternfly.min.css' %}"> <link rel="stylesheet" type="text/css" href="{% static 'dist/patternfly.min.css' %}">
{% include "base/header_js.html" %}
{% endblock %} {% endblock %}
{% block head %} {% block head %}

View File

@ -44,11 +44,9 @@ def create_test_tenant() -> Tenant:
return Tenant.objects.create(domain=uid, default=True) 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""" """Generate a certificate for testing"""
builder = CertificateBuilder( builder = CertificateBuilder()
use_ec_private_key=use_ec_private_key,
)
builder.common_name = "goauthentik.io" builder.common_name = "goauthentik.io"
builder.build( builder.build(
subject_alt_names=["goauthentik.io"], subject_alt_names=["goauthentik.io"],

View File

@ -1,5 +1,4 @@
"""Crypto API Views""" """Crypto API Views"""
from datetime import datetime
from typing import Optional from typing import Optional
from cryptography.hazmat.backends import default_backend from cryptography.hazmat.backends import default_backend
@ -35,10 +34,7 @@ LOGGER = get_logger()
class CertificateKeyPairSerializer(ModelSerializer): class CertificateKeyPairSerializer(ModelSerializer):
"""CertificateKeyPair Serializer""" """CertificateKeyPair Serializer"""
fingerprint_sha256 = SerializerMethodField() cert_expiry = DateTimeField(source="certificate.not_valid_after", read_only=True)
fingerprint_sha1 = SerializerMethodField()
cert_expiry = SerializerMethodField()
cert_subject = SerializerMethodField() cert_subject = SerializerMethodField()
private_key_available = SerializerMethodField() private_key_available = SerializerMethodField()
private_key_type = SerializerMethodField() private_key_type = SerializerMethodField()
@ -46,35 +42,8 @@ class CertificateKeyPairSerializer(ModelSerializer):
certificate_download_url = SerializerMethodField() certificate_download_url = SerializerMethodField()
private_key_download_url = SerializerMethodField() private_key_download_url = SerializerMethodField()
@property def get_cert_subject(self, instance: CertificateKeyPair) -> str:
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]:
"""Get certificate subject as full rfc4514""" """Get certificate subject as full rfc4514"""
if not self._should_include_details:
return None
return instance.certificate.subject.rfc4514_string() return instance.certificate.subject.rfc4514_string()
def get_private_key_available(self, instance: CertificateKeyPair) -> bool: def get_private_key_available(self, instance: CertificateKeyPair) -> bool:
@ -83,8 +52,6 @@ class CertificateKeyPairSerializer(ModelSerializer):
def get_private_key_type(self, instance: CertificateKeyPair) -> Optional[str]: def get_private_key_type(self, instance: CertificateKeyPair) -> Optional[str]:
"""Get the private key's type, if set""" """Get the private key's type, if set"""
if not self._should_include_details:
return None
key = instance.private_key key = instance.private_key
if key: if key:
return key.__class__.__name__.replace("_", "").lower().replace("privatekey", "") return key.__class__.__name__.replace("_", "").lower().replace("privatekey", "")
@ -204,14 +171,6 @@ class CertificateKeyPairViewSet(UsedByMixin, ModelViewSet):
ordering = ["name"] ordering = ["name"]
search_fields = ["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"]) @permission_required(None, ["authentik_crypto.add_certificatekeypair"])
@extend_schema( @extend_schema(
request=CertificateGenerationSerializer(), request=CertificateGenerationSerializer(),

View File

@ -6,8 +6,7 @@ from typing import Optional
from cryptography import x509 from cryptography import x509
from cryptography.hazmat.backends import default_backend from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import ec, rsa from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives.asymmetric.types import PRIVATE_KEY_TYPES
from cryptography.x509.oid import NameOID from cryptography.x509.oid import NameOID
from authentik import __version__ from authentik import __version__
@ -19,10 +18,7 @@ class CertificateBuilder:
common_name: str common_name: str
_use_ec_private_key: bool def __init__(self):
def __init__(self, use_ec_private_key=False):
self._use_ec_private_key = use_ec_private_key
self.__public_key = None self.__public_key = None
self.__private_key = None self.__private_key = None
self.__builder = None self.__builder = None
@ -40,14 +36,6 @@ class CertificateBuilder:
self.cert.save() self.cert.save()
return self.cert 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( def build(
self, self,
validity_days: int = 365, validity_days: int = 365,
@ -55,7 +43,9 @@ class CertificateBuilder:
): ):
"""Build self-signed certificate""" """Build self-signed certificate"""
one_day = datetime.timedelta(1, 0, 0) 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() self.__public_key = self.__private_key.public_key()
alt_names: list[x509.GeneralName] = [x509.DNSName(x) for x in subject_alt_names or []] alt_names: list[x509.GeneralName] = [x509.DNSName(x) for x in subject_alt_names or []]
self.__builder = ( self.__builder = (

View File

@ -38,9 +38,9 @@ class Command(BaseCommand):
try: try:
serializer.validate_certificate_data(keypair.certificate_data) serializer.validate_certificate_data(keypair.certificate_data)
if keypair.key_data != "": if keypair.key_data != "":
serializer.validate_key_data(keypair.key_data) serializer.validate_certificate_data(keypair.key_data)
except ValidationError as exc: except ValidationError as exc:
self.stderr.write(str(exc)) self.stderr.write(exc)
sys_exit(1) sys_exit(1)
if dirty: if dirty:
keypair.save() keypair.save()

View File

@ -1,6 +1,5 @@
"""Crypto tests""" """Crypto tests"""
import datetime import datetime
from json import loads
from os import makedirs from os import makedirs
from tempfile import TemporaryDirectory from tempfile import TemporaryDirectory
@ -87,35 +86,13 @@ class TestCrypto(APITestCase):
def test_list(self): def test_list(self):
"""Test API List""" """Test API List"""
cert = create_test_cert()
self.client.force_login(create_test_admin_user()) self.client.force_login(create_test_admin_user())
response = self.client.get( response = self.client.get(
reverse( reverse(
"authentik_api:certificatekeypair-list", "authentik_api:certificatekeypair-list",
) )
+ f"?name={cert.name}"
) )
self.assertEqual(200, response.status_code) 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): def test_certificate_download(self):
"""Test certificate export (download)""" """Test certificate export (download)"""

View File

@ -0,0 +1,70 @@
# Generated by Django 3.0.6 on 2020-05-19 22:08
import uuid
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name="Event",
fields=[
(
"event_uuid",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
(
"action",
models.TextField(
choices=[
("LOGIN", "login"),
("LOGIN_FAILED", "login_failed"),
("LOGOUT", "logout"),
("AUTHORIZE_APPLICATION", "authorize_application"),
("SUSPICIOUS_REQUEST", "suspicious_request"),
("SIGN_UP", "sign_up"),
("PASSWORD_RESET", "password_reset"),
("INVITE_CREATED", "invitation_created"),
("INVITE_USED", "invitation_used"),
("CUSTOM", "custom"),
]
),
),
("date", models.DateTimeField(auto_now_add=True)),
("app", models.TextField()),
(
"context",
models.JSONField(blank=True, default=dict),
),
("client_ip", models.GenericIPAddressField(null=True)),
("created", models.DateTimeField(auto_now_add=True)),
(
"user",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"verbose_name": "Event",
"verbose_name_plural": "Events",
},
),
]

View File

@ -22,8 +22,4 @@ class Migration(migrations.Migration):
default="local", default="local",
), ),
), ),
migrations.AlterModelOptions(
name="notificationwebhookmapping",
options={"verbose_name": "Webhook Mapping", "verbose_name_plural": "Webhook Mappings"},
),
] ]

View File

@ -0,0 +1,33 @@
# Generated by Django 3.1.1 on 2020-09-18 21:16
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_events", "0001_initial"),
]
operations = [
migrations.AlterField(
model_name="event",
name="action",
field=models.TextField(
choices=[
("LOGIN", "login"),
("LOGIN_FAILED", "login_failed"),
("LOGOUT", "logout"),
("AUTHORIZE_APPLICATION", "authorize_application"),
("SUSPICIOUS_REQUEST", "suspicious_request"),
("SIGN_UP", "sign_up"),
("PASSWORD_RESET", "password_reset"),
("INVITE_CREATED", "invitation_created"),
("INVITE_USED", "invitation_used"),
("IMPERSONATION_STARTED", "impersonation_started"),
("IMPERSONATION_ENDED", "impersonation_ended"),
("CUSTOM", "custom"),
]
),
),
]

View File

@ -0,0 +1,60 @@
# Generated by Django 3.1.1 on 2020-09-17 11:55
from django.apps.registry import Apps
from django.db import migrations, models
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
import authentik.events.models
def convert_user_to_json(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
Event = apps.get_model("authentik_events", "Event")
db_alias = schema_editor.connection.alias
for event in Event.objects.using(db_alias).all():
event.delete()
# Because event objects cannot be updated, we have to re-create them
event.pk = None
event.user_json = authentik.events.models.get_user(event.user) if event.user else {}
event._state.adding = True
event.save()
class Migration(migrations.Migration):
dependencies = [
("authentik_events", "0002_auto_20200918_2116"),
]
operations = [
migrations.AlterField(
model_name="event",
name="action",
field=models.TextField(
choices=[
("LOGIN", "login"),
("LOGIN_FAILED", "login_failed"),
("LOGOUT", "logout"),
("AUTHORIZE_APPLICATION", "authorize_application"),
("SUSPICIOUS_REQUEST", "suspicious_request"),
("SIGN_UP", "sign_up"),
("PASSWORD_RESET", "password_reset"),
("INVITE_CREATED", "invitation_created"),
("INVITE_USED", "invitation_used"),
("IMPERSONATION_STARTED", "impersonation_started"),
("IMPERSONATION_ENDED", "impersonation_ended"),
("CUSTOM", "custom"),
]
),
),
migrations.AddField(
model_name="event",
name="user_json",
field=models.JSONField(default=dict),
),
migrations.RunPython(convert_user_to_json),
migrations.RemoveField(
model_name="event",
name="user",
),
migrations.RenameField(model_name="event", old_name="user_json", new_name="user"),
]

View File

@ -0,0 +1,37 @@
# Generated by Django 3.1.1 on 2020-09-21 18:29
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_events", "0003_auto_20200917_1155"),
]
operations = [
migrations.AlterField(
model_name="event",
name="action",
field=models.TextField(
choices=[
("login", "Login"),
("login_failed", "Login Failed"),
("logout", "Logout"),
("sign_up", "Sign Up"),
("authorize_application", "Authorize Application"),
("suspicious_request", "Suspicious Request"),
("password_set", "Password Set"),
("invitation_created", "Invite Created"),
("invitation_used", "Invite Used"),
("source_linked", "Source Linked"),
("impersonation_started", "Impersonation Started"),
("impersonation_ended", "Impersonation Ended"),
("model_created", "Model Created"),
("model_updated", "Model Updated"),
("model_deleted", "Model Deleted"),
("custom_", "Custom Prefix"),
]
),
),
]

View File

@ -0,0 +1,37 @@
# Generated by Django 3.1.2 on 2020-10-05 21:39
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_events", "0004_auto_20200921_1829"),
]
operations = [
migrations.AlterField(
model_name="event",
name="action",
field=models.TextField(
choices=[
("login", "Login"),
("login_failed", "Login Failed"),
("logout", "Logout"),
("user_write", "User Write"),
("suspicious_request", "Suspicious Request"),
("password_set", "Password Set"),
("invitation_created", "Invite Created"),
("invitation_used", "Invite Used"),
("authorize_application", "Authorize Application"),
("source_linked", "Source Linked"),
("impersonation_started", "Impersonation Started"),
("impersonation_ended", "Impersonation Ended"),
("model_created", "Model Created"),
("model_updated", "Model Updated"),
("model_deleted", "Model Deleted"),
("custom_", "Custom Prefix"),
]
),
),
]

View File

@ -0,0 +1,42 @@
# Generated by Django 3.1.2 on 2020-10-17 20:24
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_events", "0005_auto_20201005_2139"),
]
operations = [
migrations.RemoveField(
model_name="event",
name="date",
),
migrations.AlterField(
model_name="event",
name="action",
field=models.TextField(
choices=[
("login", "Login"),
("login_failed", "Login Failed"),
("logout", "Logout"),
("user_write", "User Write"),
("suspicious_request", "Suspicious Request"),
("password_set", "Password Set"),
("token_view", "Token View"),
("invitation_created", "Invite Created"),
("invitation_used", "Invite Used"),
("authorize_application", "Authorize Application"),
("source_linked", "Source Linked"),
("impersonation_started", "Impersonation Started"),
("impersonation_ended", "Impersonation Ended"),
("model_created", "Model Created"),
("model_updated", "Model Updated"),
("model_deleted", "Model Deleted"),
("custom_", "Custom Prefix"),
]
),
),
]

View File

@ -0,0 +1,41 @@
# Generated by Django 3.1.4 on 2020-12-15 09:39
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_events", "0006_auto_20201017_2024"),
]
operations = [
migrations.AlterField(
model_name="event",
name="action",
field=models.TextField(
choices=[
("login", "Login"),
("login_failed", "Login Failed"),
("logout", "Logout"),
("user_write", "User Write"),
("suspicious_request", "Suspicious Request"),
("password_set", "Password Set"),
("token_view", "Token View"),
("invitation_created", "Invite Created"),
("invitation_used", "Invite Used"),
("authorize_application", "Authorize Application"),
("source_linked", "Source Linked"),
("impersonation_started", "Impersonation Started"),
("impersonation_ended", "Impersonation Ended"),
("policy_execution", "Policy Execution"),
("policy_exception", "Policy Exception"),
("property_mapping_exception", "Property Mapping Exception"),
("model_created", "Model Created"),
("model_updated", "Model Updated"),
("model_deleted", "Model Deleted"),
("custom_", "Custom Prefix"),
]
),
),
]

View File

@ -0,0 +1,42 @@
# Generated by Django 3.1.4 on 2020-12-20 16:51
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_events", "0007_auto_20201215_0939"),
]
operations = [
migrations.AlterField(
model_name="event",
name="action",
field=models.TextField(
choices=[
("login", "Login"),
("login_failed", "Login Failed"),
("logout", "Logout"),
("user_write", "User Write"),
("suspicious_request", "Suspicious Request"),
("password_set", "Password Set"),
("token_view", "Token View"),
("invitation_created", "Invite Created"),
("invitation_used", "Invite Used"),
("authorize_application", "Authorize Application"),
("source_linked", "Source Linked"),
("impersonation_started", "Impersonation Started"),
("impersonation_ended", "Impersonation Ended"),
("policy_execution", "Policy Execution"),
("policy_exception", "Policy Exception"),
("property_mapping_exception", "Property Mapping Exception"),
("model_created", "Model Created"),
("model_updated", "Model Updated"),
("model_deleted", "Model Deleted"),
("update_available", "Update Available"),
("custom_", "Custom Prefix"),
]
),
),
]

View File

@ -0,0 +1,42 @@
# Generated by Django 3.1.4 on 2020-12-27 12:10
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_events", "0008_auto_20201220_1651"),
]
operations = [
migrations.AlterField(
model_name="event",
name="action",
field=models.TextField(
choices=[
("login", "Login"),
("login_failed", "Login Failed"),
("logout", "Logout"),
("user_write", "User Write"),
("suspicious_request", "Suspicious Request"),
("password_set", "Password Set"),
("token_view", "Token View"),
("invitation_used", "Invite Used"),
("authorize_application", "Authorize Application"),
("source_linked", "Source Linked"),
("impersonation_started", "Impersonation Started"),
("impersonation_ended", "Impersonation Ended"),
("policy_execution", "Policy Execution"),
("policy_exception", "Policy Exception"),
("property_mapping_exception", "Property Mapping Exception"),
("configuration_error", "Configuration Error"),
("model_created", "Model Created"),
("model_updated", "Model Updated"),
("model_deleted", "Model Deleted"),
("update_available", "Update Available"),
("custom_", "Custom Prefix"),
]
),
),
]

View File

@ -0,0 +1,148 @@
# Generated by Django 3.1.4 on 2021-01-11 16:36
import uuid
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("authentik_policies", "0004_policy_execution_logging"),
("authentik_core", "0016_auto_20201202_2234"),
("authentik_events", "0009_auto_20201227_1210"),
]
operations = [
migrations.CreateModel(
name="NotificationTransport",
fields=[
(
"uuid",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
("name", models.TextField(unique=True)),
(
"mode",
models.TextField(
choices=[
("webhook", "Generic Webhook"),
("webhook_slack", "Slack Webhook (Slack/Discord)"),
("email", "Email"),
]
),
),
("webhook_url", models.TextField(blank=True)),
],
options={
"verbose_name": "Notification Transport",
"verbose_name_plural": "Notification Transports",
},
),
migrations.CreateModel(
name="NotificationRule",
fields=[
(
"policybindingmodel_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="authentik_policies.policybindingmodel",
),
),
("name", models.TextField(unique=True)),
(
"severity",
models.TextField(
choices=[
("notice", "Notice"),
("warning", "Warning"),
("alert", "Alert"),
],
default="notice",
help_text="Controls which severity level the created notifications will have.",
),
),
(
"group",
models.ForeignKey(
blank=True,
help_text="Define which group of users this notification should be sent and shown to. If left empty, Notification won't ben sent.",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="authentik_core.group",
),
),
(
"transports",
models.ManyToManyField(
help_text="Select which transports should be used to notify the user. If none are selected, the notification will only be shown in the authentik UI.",
to="authentik_events.NotificationTransport",
),
),
],
options={
"verbose_name": "Notification Rule",
"verbose_name_plural": "Notification Rules",
},
bases=("authentik_policies.policybindingmodel",),
),
migrations.CreateModel(
name="Notification",
fields=[
(
"uuid",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
(
"severity",
models.TextField(
choices=[
("notice", "Notice"),
("warning", "Warning"),
("alert", "Alert"),
]
),
),
("body", models.TextField()),
("created", models.DateTimeField(auto_now_add=True)),
("seen", models.BooleanField(default=False)),
(
"event",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="authentik_events.event",
),
),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"verbose_name": "Notification",
"verbose_name_plural": "Notifications",
},
),
]

View File

@ -0,0 +1,15 @@
# Generated by Django 3.1.4 on 2021-01-10 18:57
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
(
"authentik_events",
"0010_notification_notificationtransport_notificationrule",
),
]
operations = []

View File

@ -0,0 +1,52 @@
# Generated by Django 3.1.6 on 2021-02-02 18:21
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_events", "0011_notification_rules_default_v1"),
]
operations = [
migrations.AddField(
model_name="notificationtransport",
name="send_once",
field=models.BooleanField(
default=False,
help_text="Only send notification once, for example when sending a webhook into a chat channel.",
),
),
migrations.AlterField(
model_name="event",
name="action",
field=models.TextField(
choices=[
("login", "Login"),
("login_failed", "Login Failed"),
("logout", "Logout"),
("user_write", "User Write"),
("suspicious_request", "Suspicious Request"),
("password_set", "Password Set"),
("token_view", "Token View"),
("invitation_used", "Invite Used"),
("authorize_application", "Authorize Application"),
("source_linked", "Source Linked"),
("impersonation_started", "Impersonation Started"),
("impersonation_ended", "Impersonation Ended"),
("policy_execution", "Policy Execution"),
("policy_exception", "Policy Exception"),
("property_mapping_exception", "Property Mapping Exception"),
("system_task_execution", "System Task Execution"),
("system_task_exception", "System Task Exception"),
("configuration_error", "Configuration Error"),
("model_created", "Model Created"),
("model_updated", "Model Updated"),
("model_deleted", "Model Deleted"),
("update_available", "Update Available"),
("custom_", "Custom Prefix"),
]
),
),
]

View File

@ -0,0 +1,61 @@
# Generated by Django 3.1.6 on 2021-02-09 16:57
from django.apps.registry import Apps
from django.db import migrations, models
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
def token_view_to_secret_view(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
from authentik.events.models import EventAction
db_alias = schema_editor.connection.alias
Event = apps.get_model("authentik_events", "Event")
events = Event.objects.using(db_alias).filter(action="token_view")
for event in events:
event.context["secret"] = event.context.pop("token")
event.action = EventAction.SECRET_VIEW
Event.objects.using(db_alias).bulk_update(events, ["context", "action"])
class Migration(migrations.Migration):
dependencies = [
("authentik_events", "0012_auto_20210202_1821"),
]
operations = [
migrations.AlterField(
model_name="event",
name="action",
field=models.TextField(
choices=[
("login", "Login"),
("login_failed", "Login Failed"),
("logout", "Logout"),
("user_write", "User Write"),
("suspicious_request", "Suspicious Request"),
("password_set", "Password Set"),
("secret_view", "Secret View"),
("invitation_used", "Invite Used"),
("authorize_application", "Authorize Application"),
("source_linked", "Source Linked"),
("impersonation_started", "Impersonation Started"),
("impersonation_ended", "Impersonation Ended"),
("policy_execution", "Policy Execution"),
("policy_exception", "Policy Exception"),
("property_mapping_exception", "Property Mapping Exception"),
("system_task_execution", "System Task Execution"),
("system_task_exception", "System Task Exception"),
("configuration_error", "Configuration Error"),
("model_created", "Model Created"),
("model_updated", "Model Updated"),
("model_deleted", "Model Deleted"),
("update_available", "Update Available"),
("custom_", "Custom Prefix"),
]
),
),
migrations.RunPython(token_view_to_secret_view),
]

View File

@ -0,0 +1,87 @@
# Generated by Django 3.1.7 on 2021-03-18 16:01
from datetime import timedelta
from typing import Iterable
from django.apps.registry import Apps
from django.db import migrations, models
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
import authentik.events.models
# Taken from https://stackoverflow.com/questions/3173320/text-progress-bar-in-the-console
def progress_bar(
iterable: Iterable,
prefix="Writing: ",
suffix=" finished",
decimals=1,
length=100,
fill="",
print_end="\r",
):
"""
Call in a loop to create terminal progress bar
@params:
iteration - Required : current iteration (Int)
total - Required : total iterations (Int)
prefix - Optional : prefix string (Str)
suffix - Optional : suffix string (Str)
decimals - Optional : positive number of decimals in percent complete (Int)
length - Optional : character length of bar (Int)
fill - Optional : bar fill character (Str)
print_end - Optional : end character (e.g. "\r", "\r\n") (Str)
"""
total = len(iterable)
if total < 1:
return
def print_progress_bar(iteration):
"""Progress Bar Printing Function"""
percent = ("{0:." + str(decimals) + "f}").format(100 * (iteration / float(total)))
filledLength = int(length * iteration // total)
bar = fill * filledLength + "-" * (length - filledLength)
print(f"\r{prefix} |{bar}| {percent}% {suffix}", end=print_end)
# Initial Call
print_progress_bar(0)
# Update Progress Bar
for i, item in enumerate(iterable):
yield item
print_progress_bar(i + 1)
# Print New Line on Complete
print()
def update_expires(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
db_alias = schema_editor.connection.alias
Event = apps.get_model("authentik_events", "event")
all_events = Event.objects.using(db_alias).all()
if all_events.count() < 1:
return
print("\nAdding expiry to events, this might take a couple of minutes...")
for event in progress_bar(all_events):
event.expires = event.created + timedelta(days=365)
event.save()
class Migration(migrations.Migration):
dependencies = [
("authentik_events", "0013_auto_20210209_1657"),
]
operations = [
migrations.AddField(
model_name="event",
name="expires",
field=models.DateTimeField(default=authentik.events.models.default_event_duration),
),
migrations.AddField(
model_name="event",
name="expiring",
field=models.BooleanField(default=True),
),
migrations.RunPython(update_expires),
]

View File

@ -0,0 +1,45 @@
# Generated by Django 3.2.3 on 2021-06-09 07:58
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_events", "0014_expiry"),
]
operations = [
migrations.AlterField(
model_name="event",
name="action",
field=models.TextField(
choices=[
("login", "Login"),
("login_failed", "Login Failed"),
("logout", "Logout"),
("user_write", "User Write"),
("suspicious_request", "Suspicious Request"),
("password_set", "Password Set"),
("secret_view", "Secret View"),
("invitation_used", "Invite Used"),
("authorize_application", "Authorize Application"),
("source_linked", "Source Linked"),
("impersonation_started", "Impersonation Started"),
("impersonation_ended", "Impersonation Ended"),
("policy_execution", "Policy Execution"),
("policy_exception", "Policy Exception"),
("property_mapping_exception", "Property Mapping Exception"),
("system_task_execution", "System Task Execution"),
("system_task_exception", "System Task Exception"),
("configuration_error", "Configuration Error"),
("model_created", "Model Created"),
("model_updated", "Model Updated"),
("model_deleted", "Model Deleted"),
("email_sent", "Email Sent"),
("update_available", "Update Available"),
("custom_", "Custom Prefix"),
]
),
),
]

View File

@ -0,0 +1,53 @@
# Generated by Django 3.2.4 on 2021-06-14 15:33
from django.db import migrations, models
import authentik.events.models
class Migration(migrations.Migration):
dependencies = [
("authentik_events", "0015_alter_event_action"),
]
operations = [
migrations.AddField(
model_name="event",
name="tenant",
field=models.JSONField(blank=True, default=authentik.events.models.default_tenant),
),
migrations.AlterField(
model_name="event",
name="action",
field=models.TextField(
choices=[
("login", "Login"),
("login_failed", "Login Failed"),
("logout", "Logout"),
("user_write", "User Write"),
("suspicious_request", "Suspicious Request"),
("password_set", "Password Set"),
("secret_view", "Secret View"),
("invitation_used", "Invite Used"),
("authorize_application", "Authorize Application"),
("source_linked", "Source Linked"),
("impersonation_started", "Impersonation Started"),
("impersonation_ended", "Impersonation Ended"),
("policy_execution", "Policy Execution"),
("policy_exception", "Policy Exception"),
("property_mapping_exception", "Property Mapping Exception"),
("system_task_execution", "System Task Execution"),
("system_task_exception", "System Task Exception"),
("system_exception", "System Exception"),
("configuration_error", "Configuration Error"),
("model_created", "Model Created"),
("model_updated", "Model Updated"),
("model_deleted", "Model Deleted"),
("email_sent", "Email Sent"),
("update_available", "Update Available"),
("custom_", "Custom Prefix"),
]
),
),
]

View File

@ -0,0 +1,47 @@
# Generated by Django 3.2.5 on 2021-07-14 19:15
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_events", "0016_add_tenant"),
]
operations = [
migrations.AlterField(
model_name="event",
name="action",
field=models.TextField(
choices=[
("login", "Login"),
("login_failed", "Login Failed"),
("logout", "Logout"),
("user_write", "User Write"),
("suspicious_request", "Suspicious Request"),
("password_set", "Password Set"),
("secret_view", "Secret View"),
("secret_rotate", "Secret Rotate"),
("invitation_used", "Invite Used"),
("authorize_application", "Authorize Application"),
("source_linked", "Source Linked"),
("impersonation_started", "Impersonation Started"),
("impersonation_ended", "Impersonation Ended"),
("policy_execution", "Policy Execution"),
("policy_exception", "Policy Exception"),
("property_mapping_exception", "Property Mapping Exception"),
("system_task_execution", "System Task Execution"),
("system_task_exception", "System Task Exception"),
("system_exception", "System Exception"),
("configuration_error", "Configuration Error"),
("model_created", "Model Created"),
("model_updated", "Model Updated"),
("model_deleted", "Model Deleted"),
("email_sent", "Email Sent"),
("update_available", "Update Available"),
("custom_", "Custom Prefix"),
]
),
),
]

View File

@ -0,0 +1,46 @@
# Generated by Django 3.2.6 on 2021-09-11 22:17
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_core", "0028_alter_token_intent"),
("authentik_events", "0017_alter_event_action"),
]
operations = [
migrations.CreateModel(
name="NotificationWebhookMapping",
fields=[
(
"propertymapping_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="authentik_core.propertymapping",
),
),
],
options={
"verbose_name": "Notification Webhook Mapping",
"verbose_name_plural": "Notification Webhook Mappings",
},
bases=("authentik_core.propertymapping",),
),
migrations.AddField(
model_name="notificationtransport",
name="webhook_mapping",
field=models.ForeignKey(
default=None,
null=True,
on_delete=django.db.models.deletion.SET_DEFAULT,
to="authentik_events.notificationwebhookmapping",
),
),
]

View File

@ -0,0 +1,22 @@
# Generated by Django 3.2.7 on 2021-10-04 15:31
from django.db import migrations, models
import authentik.lib.models
class Migration(migrations.Migration):
dependencies = [
("authentik_events", "0018_auto_20210911_2217"),
]
operations = [
migrations.AlterField(
model_name="notificationtransport",
name="webhook_url",
field=models.TextField(
blank=True, validators=[authentik.lib.models.DomainlessURLValidator()]
),
),
]

View File

@ -445,9 +445,8 @@ class NotificationTransport(SerializerModel):
subject += notification.body[:75] subject += notification.body[:75]
mail = TemplateEmailMessage( mail = TemplateEmailMessage(
subject=subject, subject=subject,
to=[notification.user.email],
language=notification.user.locale(),
template_name="email/generic.html", template_name="email/generic.html",
to=[notification.user.email],
template_context={ template_context={
"title": subject, "title": subject,
"body": notification.body, "body": notification.body,
@ -561,7 +560,7 @@ class NotificationRule(SerializerModel, PolicyBindingModel):
class NotificationWebhookMapping(PropertyMapping): class NotificationWebhookMapping(PropertyMapping):
"""Modify the payload of outgoing webhook requests""" """Modify the schema and layout of the webhook being sent"""
@property @property
def component(self) -> str: def component(self) -> str:
@ -574,9 +573,9 @@ class NotificationWebhookMapping(PropertyMapping):
return NotificationWebhookMappingSerializer return NotificationWebhookMappingSerializer
def __str__(self): def __str__(self):
return f"Webhook Mapping {self.name}" return f"Notification Webhook Mapping {self.name}"
class Meta: class Meta:
verbose_name = _("Webhook Mapping") verbose_name = _("Notification Webhook Mapping")
verbose_name_plural = _("Webhook Mappings") verbose_name_plural = _("Notification Webhook Mappings")

View File

@ -14,7 +14,6 @@ from django.views.debug import SafeExceptionReporterFilter
from geoip2.models import City from geoip2.models import City
from guardian.utils import get_anonymous_user from guardian.utils import get_anonymous_user
from authentik.blueprints.v1.common import YAMLTag
from authentik.core.models import User from authentik.core.models import User
from authentik.events.geo import GEOIP_READER from authentik.events.geo import GEOIP_READER
from authentik.policies.types import PolicyRequest from authentik.policies.types import PolicyRequest
@ -112,10 +111,6 @@ def sanitize_item(value: Any) -> Any:
return GEOIP_READER.city_to_dict(value) return GEOIP_READER.city_to_dict(value)
if isinstance(value, Path): if isinstance(value, Path):
return str(value) return str(value)
if isinstance(value, Exception):
return str(value)
if isinstance(value, YAMLTag):
return str(value)
if isinstance(value, type): if isinstance(value, type):
return { return {
"type": value.__name__, "type": value.__name__,

View File

@ -7,7 +7,7 @@ from django.utils.translation import gettext as _
from drf_spectacular.types import OpenApiTypes from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiResponse, extend_schema from drf_spectacular.utils import OpenApiResponse, extend_schema
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.fields import BooleanField, DictField, ListField, ReadOnlyField from rest_framework.fields import ReadOnlyField
from rest_framework.parsers import MultiPartParser from rest_framework.parsers import MultiPartParser
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
@ -24,9 +24,7 @@ from authentik.core.api.utils import (
FilePathSerializer, FilePathSerializer,
FileUploadSerializer, FileUploadSerializer,
LinkSerializer, LinkSerializer,
PassiveSerializer,
) )
from authentik.events.utils import sanitize_dict
from authentik.flows.api.flows_diagram import FlowDiagram, FlowDiagramSerializer from authentik.flows.api.flows_diagram import FlowDiagram, FlowDiagramSerializer
from authentik.flows.exceptions import FlowNonApplicableException from authentik.flows.exceptions import FlowNonApplicableException
from authentik.flows.models import Flow from authentik.flows.models import Flow
@ -40,9 +38,10 @@ LOGGER = get_logger()
class FlowSerializer(ModelSerializer): class FlowSerializer(ModelSerializer):
"""Flow Serializer""" """Flow Serializer"""
cache_count = SerializerMethodField()
background = ReadOnlyField(source="background_url") background = ReadOnlyField(source="background_url")
cache_count = SerializerMethodField()
export_url = SerializerMethodField() export_url = SerializerMethodField()
def get_cache_count(self, flow: Flow) -> int: def get_cache_count(self, flow: Flow) -> int:
@ -72,46 +71,16 @@ class FlowSerializer(ModelSerializer):
"export_url", "export_url",
"layout", "layout",
"denied_action", "denied_action",
"authentication",
] ]
extra_kwargs = { extra_kwargs = {
"background": {"read_only": True}, "background": {"read_only": True},
} }
class FlowSetSerializer(FlowSerializer):
"""Stripped down flow serializer"""
class Meta:
model = Flow
fields = [
"pk",
"policybindingmodel_ptr_id",
"name",
"slug",
"title",
"designation",
"background",
"policy_engine_mode",
"compatibility_mode",
"export_url",
"layout",
"denied_action",
]
class FlowImportResultSerializer(PassiveSerializer):
"""Logs of an attempted flow import"""
logs = ListField(child=DictField(), read_only=True)
success = BooleanField(read_only=True)
class FlowViewSet(UsedByMixin, ModelViewSet): class FlowViewSet(UsedByMixin, ModelViewSet):
"""Flow Viewset""" """Flow Viewset"""
queryset = Flow.objects.all().prefetch_related("stages", "policies") queryset = Flow.objects.all()
serializer_class = FlowSerializer serializer_class = FlowSerializer
lookup_field = "slug" lookup_field = "slug"
ordering = ["slug", "name"] ordering = ["slug", "name"]
@ -161,38 +130,25 @@ class FlowViewSet(UsedByMixin, ModelViewSet):
@extend_schema( @extend_schema(
request={"multipart/form-data": FileUploadSerializer}, request={"multipart/form-data": FileUploadSerializer},
responses={ responses={
204: FlowImportResultSerializer, 204: OpenApiResponse(description="Successfully imported flow"),
400: FlowImportResultSerializer, 400: OpenApiResponse(description="Bad request"),
}, },
) )
@action(url_path="import", detail=False, methods=["POST"], parser_classes=(MultiPartParser,)) @action(detail=False, methods=["POST"], parser_classes=(MultiPartParser,))
def import_flow(self, request: Request) -> Response: def import_flow(self, request: Request) -> Response:
"""Import flow from .yaml file""" """Import flow from .yaml file"""
import_response = FlowImportResultSerializer(
data={
"logs": [],
"success": False,
}
)
import_response.is_valid()
file = request.FILES.get("file", None) file = request.FILES.get("file", None)
if not file: if not file:
return Response(data=import_response.initial_data, status=400) return HttpResponseBadRequest()
importer = Importer(file.read().decode()) importer = Importer(file.read().decode())
valid, logs = importer.validate() valid, _logs = importer.validate()
import_response.initial_data["logs"] = [sanitize_dict(log) for log in logs] # TODO: return logs
import_response.initial_data["success"] = valid
import_response.is_valid()
if not valid: if not valid:
return Response(data=import_response.initial_data, status=200) return HttpResponseBadRequest()
successful = importer.apply() successful = importer.apply()
import_response.initial_data["success"] = successful
import_response.is_valid()
if not successful: if not successful:
return Response(data=import_response.initial_data, status=200) return HttpResponseBadRequest()
return Response(data=import_response.initial_data, status=200) return Response(status=204)
@permission_required( @permission_required(
"authentik_flows.export_flow", "authentik_flows.export_flow",
@ -262,11 +218,7 @@ class FlowViewSet(UsedByMixin, ModelViewSet):
return Response({}) return Response({})
if background: if background:
flow.background = background flow.background = background
try: flow.save()
flow.save()
except PermissionError as exc:
LOGGER.warning("Failed to save icon", exc=exc)
return HttpResponseBadRequest()
return Response({}) return Response({})
return HttpResponseBadRequest() return HttpResponseBadRequest()

View File

@ -12,7 +12,7 @@ from structlog.stdlib import get_logger
from authentik.core.api.used_by import UsedByMixin from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import MetaNameSerializer, TypeCreateSerializer from authentik.core.api.utils import MetaNameSerializer, TypeCreateSerializer
from authentik.core.types import UserSettingSerializer from authentik.core.types import UserSettingSerializer
from authentik.flows.api.flows import FlowSetSerializer from authentik.flows.api.flows import FlowSerializer
from authentik.flows.models import ConfigurableStage, Stage from authentik.flows.models import ConfigurableStage, Stage
from authentik.lib.utils.reflection import all_subclasses from authentik.lib.utils.reflection import all_subclasses
@ -23,7 +23,7 @@ class StageSerializer(ModelSerializer, MetaNameSerializer):
"""Stage Serializer""" """Stage Serializer"""
component = SerializerMethodField() component = SerializerMethodField()
flow_set = FlowSetSerializer(many=True, required=False) flow_set = FlowSerializer(many=True, required=False)
def get_component(self, obj: Stage) -> str: def get_component(self, obj: Stage) -> str:
"""Get object type so that we know how to edit the object""" """Get object type so that we know how to edit the object"""
@ -55,13 +55,13 @@ class StageViewSet(
): ):
"""Stage Viewset""" """Stage Viewset"""
queryset = Stage.objects.none() queryset = Stage.objects.all().select_related("flow_set")
serializer_class = StageSerializer serializer_class = StageSerializer
search_fields = ["name"] search_fields = ["name"]
filterset_fields = ["name"] filterset_fields = ["name"]
def get_queryset(self): # pragma: no cover def get_queryset(self): # pragma: no cover
return Stage.objects.select_subclasses().prefetch_related("flow_set") return Stage.objects.select_subclasses()
@extend_schema(responses={200: TypeCreateSerializer(many=True)}) @extend_schema(responses={200: TypeCreateSerializer(many=True)})
@action(detail=False, pagination_class=None, filter_backends=[]) @action(detail=False, pagination_class=None, filter_backends=[])

View File

@ -1,6 +1,4 @@
"""flow exceptions""" """flow exceptions"""
from typing import Optional
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from authentik.lib.sentry import SentryIgnoredException from authentik.lib.sentry import SentryIgnoredException
@ -8,15 +6,15 @@ from authentik.policies.types import PolicyResult
class FlowNonApplicableException(SentryIgnoredException): class FlowNonApplicableException(SentryIgnoredException):
"""Flow does not apply to current user (denied by policy, or otherwise).""" """Flow does not apply to current user (denied by policy)."""
policy_result: Optional[PolicyResult] = None policy_result: PolicyResult
@property @property
def messages(self) -> str: def messages(self) -> str:
"""Get messages from policy result, fallback to generic reason""" """Get messages from policy result, fallback to generic reason"""
if not self.policy_result or len(self.policy_result.messages) < 1: if len(self.policy_result.messages) < 1:
return _("Flow does not apply to current user.") return _("Flow does not apply to current user (denied by policy).")
return "\n".join(self.policy_result.messages) return "\n".join(self.policy_result.messages)

View File

@ -0,0 +1,138 @@
# Generated by Django 3.0.6 on 2020-05-19 22:07
import uuid
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
("authentik_policies", "0001_initial"),
]
operations = [
migrations.CreateModel(
name="Flow",
fields=[
(
"flow_uuid",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
("name", models.TextField()),
("slug", models.SlugField(unique=True)),
(
"designation",
models.CharField(
choices=[
("authentication", "Authentication"),
("invalidation", "Invalidation"),
("enrollment", "Enrollment"),
("unenrollment", "Unrenollment"),
("recovery", "Recovery"),
("password_change", "Password Change"),
],
max_length=100,
),
),
(
"pbm",
models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
related_name="+",
to="authentik_policies.PolicyBindingModel",
),
),
],
options={
"verbose_name": "Flow",
"verbose_name_plural": "Flows",
},
bases=("authentik_policies.policybindingmodel",),
),
migrations.CreateModel(
name="Stage",
fields=[
(
"stage_uuid",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
("name", models.TextField()),
],
),
migrations.CreateModel(
name="FlowStageBinding",
fields=[
(
"policybindingmodel_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
to="authentik_policies.PolicyBindingModel",
),
),
(
"fsb_uuid",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
(
"re_evaluate_policies",
models.BooleanField(
default=False,
help_text="When this option is enabled, the planner will re-evaluate policies bound to this.",
),
),
("order", models.IntegerField()),
(
"flow",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="authentik_flows.Flow",
),
),
(
"stage",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="authentik_flows.Stage",
),
),
],
options={
"verbose_name": "Flow Stage Binding",
"verbose_name_plural": "Flow Stage Bindings",
"ordering": ["order", "flow"],
"unique_together": {("flow", "stage", "order")},
},
bases=("authentik_policies.policybindingmodel",),
),
migrations.AddField(
model_name="flow",
name="stages",
field=models.ManyToManyField(
blank=True,
through="authentik_flows.FlowStageBinding",
to="authentik_flows.Stage",
),
),
]

View File

@ -0,0 +1,29 @@
# Generated by Django 3.0.6 on 2020-05-23 11:33
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_flows", "0001_initial"),
]
operations = [
migrations.AlterField(
model_name="flow",
name="designation",
field=models.CharField(
choices=[
("authentication", "Authentication"),
("authorization", "Authorization"),
("invalidation", "Invalidation"),
("enrollment", "Enrollment"),
("unenrollment", "Unrenollment"),
("recovery", "Recovery"),
("password_change", "Password Change"),
],
max_length=100,
),
),
]

View File

@ -0,0 +1,29 @@
# Generated by Django 3.0.7 on 2020-06-29 08:57
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_flows", "0003_auto_20200523_1133"),
]
operations = [
migrations.AlterField(
model_name="flow",
name="designation",
field=models.CharField(
choices=[
("authentication", "Authentication"),
("authorization", "Authorization"),
("invalidation", "Invalidation"),
("enrollment", "Enrollment"),
("unenrollment", "Unrenollment"),
("recovery", "Recovery"),
("stage_setup", "Stage Setup"),
],
max_length=100,
),
),
]

View File

@ -0,0 +1,47 @@
# Generated by Django 3.0.7 on 2020-07-03 20:59
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_policies", "0002_auto_20200528_1647"),
("authentik_flows", "0006_auto_20200629_0857"),
]
operations = [
migrations.AlterModelOptions(
name="flowstagebinding",
options={
"ordering": ["order", "target"],
"verbose_name": "Flow Stage Binding",
"verbose_name_plural": "Flow Stage Bindings",
},
),
migrations.RenameField(
model_name="flowstagebinding",
old_name="flow",
new_name="target",
),
migrations.RenameField(
model_name="flow",
old_name="pbm",
new_name="policybindingmodel_ptr",
),
migrations.AlterUniqueTogether(
name="flowstagebinding",
unique_together={("target", "stage", "order")},
),
migrations.AlterField(
model_name="flow",
name="policybindingmodel_ptr",
field=models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
to="authentik_policies.PolicyBindingModel",
),
),
]

View File

@ -0,0 +1,28 @@
# Generated by Django 3.1.1 on 2020-09-08 15:42
import django.db.models.deletion
from django.db import migrations, models
import authentik.lib.models
class Migration(migrations.Migration):
dependencies = [
("authentik_flows", "0011_flow_title"),
]
operations = [
migrations.AlterField(
model_name="flowstagebinding",
name="stage",
field=authentik.lib.models.InheritanceForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="authentik_flows.stage"
),
),
migrations.AlterField(
model_name="stage",
name="name",
field=models.TextField(unique=True),
),
]

View File

@ -0,0 +1,44 @@
# Generated by Django 3.1.1 on 2020-09-24 16:05
from django.apps.registry import Apps
from django.db import migrations, models
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
from authentik.flows.models import FlowDesignation
def update_flow_designation(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
Flow = apps.get_model("authentik_flows", "Flow")
db_alias = schema_editor.connection.alias
for flow in Flow.objects.using(db_alias).all():
if flow.designation == "stage_setup":
flow.designation = FlowDesignation.STAGE_CONFIGURATION
flow.save()
class Migration(migrations.Migration):
dependencies = [
("authentik_flows", "0012_auto_20200908_1542"),
]
operations = [
migrations.AlterField(
model_name="flow",
name="designation",
field=models.CharField(
choices=[
("authentication", "Authentication"),
("authorization", "Authorization"),
("invalidation", "Invalidation"),
("enrollment", "Enrollment"),
("unenrollment", "Unrenollment"),
("recovery", "Recovery"),
("stage_configuration", "Stage Configuration"),
],
max_length=100,
),
),
migrations.RunPython(update_flow_designation),
]

View File

@ -0,0 +1,29 @@
# Generated by Django 3.1.1 on 2020-09-25 23:32
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_flows", "0013_auto_20200924_1605"),
]
operations = [
migrations.AlterModelOptions(
name="flowstagebinding",
options={
"ordering": ["target", "order"],
"verbose_name": "Flow Stage Binding",
"verbose_name_plural": "Flow Stage Bindings",
},
),
migrations.AlterField(
model_name="flowstagebinding",
name="re_evaluate_policies",
field=models.BooleanField(
default=False,
help_text="When this option is enabled, the planner will re-evaluate policies bound to this binding.",
),
),
]

View File

@ -0,0 +1,29 @@
# Generated by Django 3.1.2 on 2020-10-20 12:42
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_flows", "0014_auto_20200925_2332"),
]
operations = [
migrations.AlterField(
model_name="flowstagebinding",
name="re_evaluate_policies",
field=models.BooleanField(
default=False,
help_text="Evaluate policies when the Stage is present to the user.",
),
),
migrations.AddField(
model_name="flowstagebinding",
name="evaluate_on_plan",
field=models.BooleanField(
default=True,
help_text="Evaluate policies during the Flow planning process. Disable this for input-based policies.",
),
),
]

View File

@ -0,0 +1,50 @@
# Generated by Django 3.1.3 on 2020-12-02 13:07
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_flows", "0015_flowstagebinding_evaluate_on_plan"),
]
operations = [
migrations.AddField(
model_name="flow",
name="background",
field=models.FileField(
blank=True,
default="../static/dist/assets/images/flow_background.jpg",
help_text="Background shown during execution",
upload_to="flow-backgrounds/",
),
),
migrations.AlterField(
model_name="flow",
name="designation",
field=models.CharField(
choices=[
("authentication", "Authentication"),
("authorization", "Authorization"),
("invalidation", "Invalidation"),
("enrollment", "Enrollment"),
("unenrollment", "Unrenollment"),
("recovery", "Recovery"),
("stage_configuration", "Stage Configuration"),
],
help_text="Decides what this Flow is used for. For example, the Authentication flow is redirect to when an un-authenticated user visits authentik.",
max_length=100,
),
),
migrations.AlterField(
model_name="flow",
name="slug",
field=models.SlugField(help_text="Visible in the URL.", unique=True),
),
migrations.AlterField(
model_name="flow",
name="title",
field=models.TextField(help_text="Shown as the Title in Flow pages."),
),
]

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