Compare commits

...

42 Commits

Author SHA1 Message Date
e25f6aea8c release: 2021.6.1-rc4 2021-06-10 18:59:00 +02:00
b1a9eda1d3 ci: fix release test using wrong docker image
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-10 18:58:30 +02:00
2c15ab9995 release: 2021.6.1-rc3 2021-06-10 18:04:59 +02:00
b3c51e426d web: fix styling for toggle group on dark mode
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-10 18:02:27 +02:00
71578af47f ci: fix testing for release tag
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-10 17:41:54 +02:00
6c985acb36 release: 2021.6.1-rc2 2021-06-10 14:10:47 +02:00
d878d2140e providers/saml: add metadata download link to api
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-10 14:06:44 +02:00
4766d6ff3d flows: add export URL to API
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-10 13:52:50 +02:00
3a64d97040 crypto: add download links as API fields
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-10 13:46:12 +02:00
2275ba3add flows: fix get_pending_user returning in-memory user when PLAN_CONTEXT_PENDING_USER_IDENTIFIER is set
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-10 12:17:46 +02:00
9f7c941426 Merge branch 'master' into next 2021-06-10 11:59:10 +02:00
34ae9e6dab API: add endpoint to show by what objects an object is used (#995)
* core: add used_by API to show what objects are affected before deletion

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

* web/elements: add support for used_by API

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

* core: add authentik_used_by_shadows to shadow other models

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

* web: implement used_by API

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

* *: fix duplicate imports

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

* core: add action field to used_by api

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

* web: add UI for used_by action

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

* web: add notice to tenant form

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

* core: fix naming in used_by

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

* web: check length for used_by

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

* core: fix used_by for non-pk models

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

* *: improve __str__ on models

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

* core: add support for many to many in used_by

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-10 11:58:12 +02:00
bf683514ee build(deps): bump @babel/plugin-proposal-decorators in /web (#1000) 2021-06-10 09:11:01 +02:00
9b58bdb447 build(deps): bump @babel/preset-env from 7.14.4 to 7.14.5 in /web (#1002) 2021-06-10 09:10:52 +02:00
4237f20ccd build(deps): bump boto3 from 1.17.90 to 1.17.91 (#1003) 2021-06-10 08:53:42 +02:00
2408719a47 build(deps): bump eslint-plugin-lit from 1.5.0 to 1.5.1 in /web (#1001) 2021-06-10 08:53:35 +02:00
b33fef7929 build(deps): bump @babel/preset-typescript from 7.13.0 to 7.14.5 in /web (#999) 2021-06-10 08:53:20 +02:00
73b9847e7d build(deps): bump @babel/core from 7.14.3 to 7.14.5 in /web (#998) 2021-06-10 08:53:10 +02:00
a7e4eb021d build(deps): bump @babel/plugin-transform-runtime in /web (#997) 2021-06-10 08:53:01 +02:00
11306770ad build(deps): bump postcss from 8.3.0 to 8.3.1 in /website (#996) 2021-06-10 08:52:51 +02:00
5235e00d3c stages/authenticator_validate: add more logging for challenges
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-09 23:58:08 +02:00
7834146efc web/admin: fix authenticatior_valiation stage not setting correct fields
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-09 19:38:54 +02:00
d4379ecd31 flows: fix configure_url not being set correctly User settings
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-09 19:25:27 +02:00
7492608ace Merge branch 'version-2021.6' into next 2021-06-09 16:06:06 +02:00
7eef501446 Revert "root: fix permissions for docker files"
This reverts commit a7adeb917e.
2021-06-09 16:04:17 +02:00
b73de96aa6 lifecycle: fix permissions for unittest xml
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-09 16:03:51 +02:00
a7adeb917e root: fix permissions for docker files
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-09 16:00:29 +02:00
4ee2f951da lifecycle: fix check_if_root not working without docker
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-09 15:56:12 +02:00
01c5235e82 ci: use bootstrap for testing
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-09 15:54:47 +02:00
0ce4f9fe12 Revert "web: don't build api client as separate bundle"
This reverts commit 7c1fe1243f.
2021-06-09 15:37:57 +02:00
2f4f951818 Revert "web: build API during npm build"
This reverts commit a6c214e8fa.
2021-06-09 15:37:50 +02:00
a6c214e8fa web: build API during npm build
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-09 15:35:35 +02:00
57f8b108c4 root: remove production=false
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-09 15:27:06 +02:00
7c1fe1243f web: don't build api client as separate bundle
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-09 15:26:42 +02:00
3f69dd34ba ci: run tests as authentik
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-09 15:05:03 +02:00
c81431895a Merge branch 'master' into version-2021.6 2021-06-09 15:04:52 +02:00
560c979d26 root: fix requirements-dev including all dependencies
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-09 14:22:45 +02:00
c5cc8842ec root: fix missing test files in docker file
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-09 14:22:32 +02:00
2a881d241d Merge branch 'master' into next 2021-06-09 11:25:07 +02:00
6291834573 outpost: fix missing outpost images
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-09 11:24:59 +02:00
eeea36acea outpost: fix missing outpost images
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-09 11:22:28 +02:00
e95b9da586 website/docs: fix beta instructions for k8s
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-06-09 11:07:02 +02:00
124 changed files with 4363 additions and 1486 deletions

View File

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

View File

@ -33,9 +33,9 @@ jobs:
with:
push: ${{ github.event_name == 'release' }}
tags: |
beryju/authentik:2021.6.1-rc1,
beryju/authentik:2021.6.1-rc4,
beryju/authentik:latest,
ghcr.io/goauthentik/server:2021.6.1-rc1,
ghcr.io/goauthentik/server:2021.6.1-rc4,
ghcr.io/goauthentik/server:latest
platforms: linux/amd64,linux/arm64
context: .
@ -66,9 +66,9 @@ jobs:
with:
push: ${{ github.event_name == 'release' }}
tags: |
beryju/authentik-proxy:2021.6.1-rc1,
beryju/authentik-proxy:2021.6.1-rc4,
beryju/authentik-proxy:latest,
ghcr.io/goauthentik/proxy:2021.6.1-rc1,
ghcr.io/goauthentik/proxy:2021.6.1-rc4,
ghcr.io/goauthentik/proxy:latest
file: outpost/proxy.Dockerfile
platforms: linux/amd64,linux/arm64
@ -99,9 +99,9 @@ jobs:
with:
push: ${{ github.event_name == 'release' }}
tags: |
beryju/authentik-ldap:2021.6.1-rc1,
beryju/authentik-ldap:2021.6.1-rc4,
beryju/authentik-ldap:latest,
ghcr.io/goauthentik/ldap:2021.6.1-rc1,
ghcr.io/goauthentik/ldap:2021.6.1-rc4,
ghcr.io/goauthentik/ldap:latest
file: outpost/ldap.Dockerfile
platforms: linux/amd64,linux/arm64
@ -122,7 +122,7 @@ jobs:
docker-compose pull -q
docker-compose up --no-start
docker-compose start postgresql redis
docker-compose run -u root --entrypoint /bin/bash server -c "apt-get update && apt-get install -y --no-install-recommends git && pip install --no-cache -r requirements-dev.txt && ./manage.py test authentik"
docker-compose run -u root server test
sentry-release:
if: ${{ github.event_name == 'release' }}
needs:
@ -138,5 +138,5 @@ jobs:
SENTRY_PROJECT: authentik
SENTRY_URL: https://sentry.beryju.org
with:
version: authentik@2021.6.1-rc1
version: authentik@2021.6.1-rc4
environment: beryjuorg-prod

View File

@ -20,11 +20,11 @@ jobs:
docker-compose pull -q
docker build \
--no-cache \
-t beryju/authentik:latest \
-t ghcr.io/goauthentik/server:latest \
-f Dockerfile .
docker-compose up --no-start
docker-compose start postgresql redis
docker-compose run -u root --entrypoint /bin/bash server -c "apt-get update && apt-get install -y --no-install-recommends git && pip install --no-cache -r requirements-dev.txt && ./manage.py test authentik"
docker-compose run -u root server test
- name: Extract version number
id: get_version
uses: actions/github-script@v4.0.2

View File

@ -8,7 +8,7 @@ WORKDIR /app/
RUN pip install pipenv && \
pipenv lock -r > requirements.txt && \
pipenv lock -rd > requirements-dev.txt
pipenv lock -r --dev-only > requirements-dev.txt
# Stage 2: Build web API
FROM openapitools/openapi-generator-cli as api-builder
@ -28,7 +28,7 @@ COPY ./web /static/
COPY --from=api-builder /local/web/api /static/api
ENV NODE_ENV=production
RUN cd /static && npm i --production=false && npm run build
RUN cd /static && npm i && npm run build
# Stage 4: Build go proxy
FROM golang:1.16.5 AS builder
@ -76,6 +76,7 @@ RUN apt-get update && \
COPY ./authentik/ /authentik
COPY ./pyproject.toml /
COPY ./xml /xml
COPY ./tests /tests
COPY ./manage.py /
COPY ./lifecycle/ /lifecycle
COPY --from=builder /work/authentik /authentik-proxy

17
Pipfile.lock generated
View File

@ -116,18 +116,17 @@
},
"boto3": {
"hashes": [
"sha256:2ade860f66fa6b9a9886d7ff2e5118e5efebc4807b863ef735d358ef730234ed",
"sha256:bbf727d770a9844834bfbf3f811db1d3438320897f67cfb21cdca5bb8fc23c13"
"sha256:4cfab400cd9ca9b27b7dffb43f5675525ea5d36c81223d64d15542fdb16cdf7e",
"sha256:b0808a58c54c595b6cc6271cbc14a09bb89f0951ca9e8b105d1e94bef3ed24a0"
],
"index": "pypi",
"version": "==1.17.90"
"version": "==1.17.91"
},
"botocore": {
"hashes": [
"sha256:6ae4ff3405cc4fc69ff3673a8dd234bf869aa556ae1e0da050d7f2aa3c3edab6",
"sha256:b301810c4bd6cab1b6eaf6bfd9f25abb27959b586c2e1689bbce035b3fb8ae66"
"sha256:462e75419e6537efb2709b7eb5b8c7ade007d30209416f0476bd7c51a2ddc78d"
],
"version": "==1.20.90"
"version": "==1.20.91"
},
"cachetools": {
"hashes": [
@ -1167,10 +1166,10 @@
},
"websocket-client": {
"hashes": [
"sha256:3e2bf58191d4619b161389a95bdce84ce9e0b24eb8107e7e590db682c2d0ca81",
"sha256:abf306dc6351dcef07f4d40453037e51cc5d9da2ef60d0fc5d0fe3bcda255372"
"sha256:b68e4959d704768fa20e35c9d508c8dc2bbc041fd8d267c0d7345cffe2824568",
"sha256:e5c333bfa9fa739538b652b6f8c8fc2559f1d364243c8a689d7c0e1d41c2e611"
],
"version": "==1.0.1"
"version": "==1.1.0"
},
"websockets": {
"hashes": [

View File

@ -1,3 +1,3 @@
"""authentik"""
__version__ = "2021.6.1-rc1"
__version__ = "2021.6.1-rc4"
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"

View File

@ -29,6 +29,7 @@ from structlog.stdlib import get_logger
from authentik.admin.api.metrics import CoordinateSerializer, get_events_per_1h
from authentik.api.decorators import permission_required
from authentik.core.api.providers import ProviderSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.models import Application, User
from authentik.events.models import EventAction
from authentik.policies.api.exec import PolicyTestResultSerializer
@ -73,7 +74,7 @@ class ApplicationSerializer(ModelSerializer):
}
class ApplicationViewSet(ModelViewSet):
class ApplicationViewSet(UsedByMixin, ModelViewSet):
"""Application Viewset"""
queryset = Application.objects.all()

View File

@ -11,6 +11,7 @@ from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import GenericViewSet
from ua_parser import user_agent_parser
from authentik.core.api.used_by import UsedByMixin
from authentik.core.models import AuthenticatedSession
from authentik.events.geo import GEOIP_READER, GeoIPDict
@ -92,6 +93,7 @@ class AuthenticatedSessionSerializer(ModelSerializer):
class AuthenticatedSessionViewSet(
mixins.RetrieveModelMixin,
mixins.DestroyModelMixin,
UsedByMixin,
mixins.ListModelMixin,
GenericViewSet,
):

View File

@ -5,6 +5,7 @@ from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
from rest_framework_guardian.filters import ObjectPermissionsFilter
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import is_dict
from authentik.core.models import Group
@ -20,7 +21,7 @@ class GroupSerializer(ModelSerializer):
fields = ["pk", "name", "is_superuser", "parent", "users", "attributes"]
class GroupViewSet(ModelViewSet):
class GroupViewSet(UsedByMixin, ModelViewSet):
"""Group Viewset"""
queryset = Group.objects.all()

View File

@ -14,6 +14,7 @@ from rest_framework.serializers import ModelSerializer, SerializerMethodField
from rest_framework.viewsets import GenericViewSet
from authentik.api.decorators import permission_required
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import (
MetaNameSerializer,
PassiveSerializer,
@ -65,6 +66,7 @@ class PropertyMappingSerializer(ManagedSerializer, ModelSerializer, MetaNameSeri
class PropertyMappingViewSet(
mixins.RetrieveModelMixin,
mixins.DestroyModelMixin,
UsedByMixin,
mixins.ListModelMixin,
GenericViewSet,
):

View File

@ -9,6 +9,7 @@ from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer, SerializerMethodField
from rest_framework.viewsets import GenericViewSet
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import MetaNameSerializer, TypeCreateSerializer
from authentik.core.models import Provider
from authentik.lib.utils.reflection import all_subclasses
@ -48,6 +49,7 @@ class ProviderSerializer(ModelSerializer, MetaNameSerializer):
class ProviderViewSet(
mixins.RetrieveModelMixin,
mixins.DestroyModelMixin,
UsedByMixin,
mixins.ListModelMixin,
GenericViewSet,
):

View File

@ -10,6 +10,7 @@ from rest_framework.serializers import ModelSerializer, SerializerMethodField
from rest_framework.viewsets import GenericViewSet
from structlog.stdlib import get_logger
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import MetaNameSerializer, TypeCreateSerializer
from authentik.core.models import Source
from authentik.core.types import UserSettingSerializer
@ -52,6 +53,7 @@ class SourceSerializer(ModelSerializer, MetaNameSerializer):
class SourceViewSet(
mixins.RetrieveModelMixin,
mixins.DestroyModelMixin,
UsedByMixin,
mixins.ListModelMixin,
GenericViewSet,
):

View File

@ -9,6 +9,7 @@ from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
from authentik.api.decorators import permission_required
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.users import UserSerializer
from authentik.core.api.utils import PassiveSerializer
from authentik.core.models import Token, TokenIntents
@ -43,7 +44,7 @@ class TokenViewSerializer(PassiveSerializer):
key = CharField(read_only=True)
class TokenViewSet(ModelViewSet):
class TokenViewSet(UsedByMixin, ModelViewSet):
"""Token Viewset"""
lookup_field = "identifier"

View File

@ -0,0 +1,102 @@
"""used_by mixin"""
from enum import Enum
from inspect import getmembers
from django.db.models.base import Model
from django.db.models.deletion import SET_DEFAULT, SET_NULL
from django.db.models.manager import Manager
from drf_spectacular.utils import extend_schema
from guardian.shortcuts import get_objects_for_user
from rest_framework.decorators import action
from rest_framework.fields import CharField, ChoiceField
from rest_framework.request import Request
from rest_framework.response import Response
from authentik.core.api.utils import PassiveSerializer
class DeleteAction(Enum):
"""Which action a delete will have on a used object"""
CASCADE = "cascade"
CASCADE_MANY = "cascade_many"
SET_NULL = "set_null"
SET_DEFAULT = "set_default"
class UsedBySerializer(PassiveSerializer):
"""A list of all objects referencing the queried object"""
app = CharField()
model_name = CharField()
pk = CharField()
name = CharField()
action = ChoiceField(choices=[(x.name, x.name) for x in DeleteAction])
def get_delete_action(manager: Manager) -> str:
"""Get the delete action from the Foreign key, falls back to cascade"""
if hasattr(manager, "field"):
if manager.field.remote_field.on_delete.__name__ == SET_NULL.__name__:
return DeleteAction.SET_NULL.name
if manager.field.remote_field.on_delete.__name__ == SET_DEFAULT.__name__:
return DeleteAction.SET_DEFAULT.name
if hasattr(manager, "source_field"):
return DeleteAction.CASCADE_MANY.name
return DeleteAction.CASCADE.name
class UsedByMixin:
"""Mixin to add a used_by endpoint to return a list of all objects using this object"""
@extend_schema(
responses={200: UsedBySerializer(many=True)},
)
@action(detail=True, pagination_class=None, filter_backends=[])
# pylint: disable=invalid-name, unused-argument, too-many-locals
def used_by(self, request: Request, *args, **kwargs) -> Response:
"""Get a list of all objects that use this object"""
# pyright: reportGeneralTypeIssues=false
model: Model = self.get_object()
used_by = []
shadows = []
for attr_name, manager in getmembers(model, lambda x: isinstance(x, Manager)):
if attr_name == "objects": # pragma: no cover
continue
manager: Manager
if manager.model._meta.abstract:
continue
app = manager.model._meta.app_label
model_name = manager.model._meta.model_name
delete_action = get_delete_action(manager)
# To make sure we only apply shadows when there are any objects,
# but so we only apply them once, have a simple flag for the first object
first_object = True
for obj in get_objects_for_user(
request.user, f"{app}.view_{model_name}", manager
).all():
# Only merge shadows on first object
if first_object:
shadows += getattr(
manager.model._meta, "authentik_used_by_shadows", []
)
first_object = False
serializer = UsedBySerializer(
data={
"app": app,
"model_name": model_name,
"pk": str(obj.pk),
"name": str(obj),
"action": delete_action,
}
)
serializer.is_valid()
used_by.append(serializer.data)
# Check the shadows map and remove anything that should be shadowed
for idx, user in enumerate(used_by):
full_model_name = f"{user['app']}.{user['model_name']}"
if full_model_name in shadows:
del used_by[idx]
return Response(used_by)

View File

@ -25,6 +25,7 @@ from rest_framework_guardian.filters import ObjectPermissionsFilter
from authentik.admin.api.metrics import CoordinateSerializer, get_events_per_1h
from authentik.api.decorators import permission_required
from authentik.core.api.groups import GroupSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import LinkSerializer, PassiveSerializer, is_dict
from authentik.core.middleware import (
SESSION_IMPERSONATE_ORIGINAL_USER,
@ -131,7 +132,7 @@ class UsersFilter(FilterSet):
fields = ["username", "name", "is_active", "is_superuser", "attributes"]
class UserViewSet(ModelViewSet):
class UserViewSet(UsedByMixin, ModelViewSet):
"""User Viewset"""
queryset = User.objects.none()

View File

@ -5,6 +5,7 @@ from typing import Any, Optional, Type
from urllib.parse import urlencode
from uuid import uuid4
import django.db.models.options as options
from django.conf import settings
from django.contrib.auth.models import AbstractUser
from django.contrib.auth.models import UserManager as DjangoUserManager
@ -41,6 +42,9 @@ GRAVATAR_URL = "https://secure.gravatar.com"
DEFAULT_AVATAR = static("dist/assets/images/user_default.png")
options.DEFAULT_NAMES = options.DEFAULT_NAMES + ("authentik_used_by_shadows",)
def default_token_duration():
"""Default duration a Token is valid"""
return now() + timedelta(minutes=30)

View File

@ -1,10 +1,12 @@
"""Crypto API Views"""
import django_filters
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.serialization import load_pem_private_key
from cryptography.x509 import load_pem_x509_certificate
from django.http.response import HttpResponse
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from django_filters import FilterSet
from django_filters.filters import BooleanFilter
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema
from rest_framework.decorators import action
@ -20,6 +22,7 @@ from rest_framework.serializers import ModelSerializer, ValidationError
from rest_framework.viewsets import ModelViewSet
from authentik.api.decorators import permission_required
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import PassiveSerializer
from authentik.crypto.builder import CertificateBuilder
from authentik.crypto.models import CertificateKeyPair
@ -33,6 +36,9 @@ class CertificateKeyPairSerializer(ModelSerializer):
cert_subject = SerializerMethodField()
private_key_available = SerializerMethodField()
certificate_download_url = SerializerMethodField()
private_key_download_url = SerializerMethodField()
def get_cert_subject(self, instance: CertificateKeyPair) -> str:
"""Get certificate subject as full rfc4514"""
return instance.certificate.subject.rfc4514_string()
@ -41,6 +47,26 @@ class CertificateKeyPairSerializer(ModelSerializer):
"""Show if this keypair has a private key configured or not"""
return instance.key_data != "" and instance.key_data is not None
def get_certificate_download_url(self, instance: CertificateKeyPair) -> str:
"""Get URL to download certificate"""
return (
reverse(
"authentik_api:certificatekeypair-view-certificate",
kwargs={"pk": instance.pk},
)
+ "?download"
)
def get_private_key_download_url(self, instance: CertificateKeyPair) -> str:
"""Get URL to download private key"""
return (
reverse(
"authentik_api:certificatekeypair-view-private-key",
kwargs={"pk": instance.pk},
)
+ "?download"
)
def validate_certificate_data(self, value: str) -> str:
"""Verify that input is a valid PEM x509 Certificate"""
try:
@ -77,6 +103,8 @@ class CertificateKeyPairSerializer(ModelSerializer):
"cert_expiry",
"cert_subject",
"private_key_available",
"certificate_download_url",
"private_key_download_url",
]
extra_kwargs = {
"key_data": {"write_only": True},
@ -100,10 +128,10 @@ class CertificateGenerationSerializer(PassiveSerializer):
validity_days = IntegerField(initial=365)
class CertificateKeyPairFilter(django_filters.FilterSet):
class CertificateKeyPairFilter(FilterSet):
"""Filter for certificates"""
has_key = django_filters.BooleanFilter(
has_key = BooleanFilter(
label="Only return certificate-key pairs with keys", method="filter_has_key"
)
@ -117,7 +145,7 @@ class CertificateKeyPairFilter(django_filters.FilterSet):
fields = ["name"]
class CertificateKeyPairViewSet(ModelViewSet):
class CertificateKeyPairViewSet(UsedByMixin, ModelViewSet):
"""CertificateKeyPair Viewset"""
queryset = CertificateKeyPair.objects.all()

View File

@ -4,10 +4,14 @@ import datetime
from django.test import TestCase
from django.urls import reverse
from authentik.core.api.used_by import DeleteAction
from authentik.core.models import User
from authentik.crypto.api import CertificateKeyPairSerializer
from authentik.crypto.builder import CertificateBuilder
from authentik.crypto.models import CertificateKeyPair
from authentik.flows.models import Flow
from authentik.providers.oauth2.generators import generate_client_secret
from authentik.providers.oauth2.models import OAuth2Provider
class TestCrypto(TestCase):
@ -91,3 +95,35 @@ class TestCrypto(TestCase):
)
self.assertEqual(200, response.status_code)
self.assertIn("Content-Disposition", response)
def test_used_by(self):
"""Test used_by endpoint"""
self.client.force_login(User.objects.get(username="akadmin"))
keypair = CertificateKeyPair.objects.first()
provider = OAuth2Provider.objects.create(
name="test",
client_id="test",
client_secret=generate_client_secret(),
authorization_flow=Flow.objects.first(),
redirect_uris="http://localhost",
rsa_key=CertificateKeyPair.objects.first(),
)
response = self.client.get(
reverse(
"authentik_api:certificatekeypair-used-by",
kwargs={"pk": keypair.pk},
)
)
self.assertEqual(200, response.status_code)
self.assertJSONEqual(
response.content.decode(),
[
{
"app": "authentik_providers_oauth2",
"model_name": "oauth2provider",
"pk": str(provider.pk),
"name": str(provider),
"action": DeleteAction.SET_NULL.name,
}
],
)

View File

@ -7,6 +7,7 @@ from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import GenericViewSet
from authentik.api.authorization import OwnerFilter, OwnerPermissions
from authentik.core.api.used_by import UsedByMixin
from authentik.events.api.event import EventSerializer
from authentik.events.models import Notification
@ -35,6 +36,7 @@ class NotificationViewSet(
mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
UsedByMixin,
mixins.ListModelMixin,
GenericViewSet,
):

View File

@ -3,6 +3,7 @@ from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.groups import GroupSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.events.models import NotificationRule
@ -24,7 +25,7 @@ class NotificationRuleSerializer(ModelSerializer):
]
class NotificationRuleViewSet(ModelViewSet):
class NotificationRuleViewSet(UsedByMixin, ModelViewSet):
"""NotificationRule Viewset"""
queryset = NotificationRule.objects.all()

View File

@ -9,6 +9,7 @@ from rest_framework.serializers import ModelSerializer, Serializer
from rest_framework.viewsets import ModelViewSet
from authentik.api.decorators import permission_required
from authentik.core.api.used_by import UsedByMixin
from authentik.events.models import (
Notification,
NotificationSeverity,
@ -52,7 +53,7 @@ class NotificationTransportTestSerializer(Serializer):
raise NotImplementedError
class NotificationTransportViewSet(ModelViewSet):
class NotificationTransportViewSet(UsedByMixin, ModelViewSet):
"""NotificationTransport Viewset"""
queryset = NotificationTransport.objects.all()

View File

@ -2,6 +2,7 @@
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.used_by import UsedByMixin
from authentik.flows.api.stages import StageSerializer
from authentik.flows.models import FlowStageBinding
@ -27,7 +28,7 @@ class FlowStageBindingSerializer(ModelSerializer):
]
class FlowStageBindingViewSet(ModelViewSet):
class FlowStageBindingViewSet(UsedByMixin, ModelViewSet):
"""FlowStageBinding Viewset"""
queryset = FlowStageBinding.objects.all()

View File

@ -24,6 +24,7 @@ from rest_framework.viewsets import ModelViewSet
from structlog.stdlib import get_logger
from authentik.api.decorators import permission_required
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import CacheSerializer, LinkSerializer
from authentik.flows.exceptions import FlowNonApplicableException
from authentik.flows.models import Flow
@ -44,10 +45,16 @@ class FlowSerializer(ModelSerializer):
background = ReadOnlyField(source="background_url")
export_url = SerializerMethodField()
def get_cache_count(self, flow: Flow) -> int:
"""Get count of cached flows"""
return len(cache.keys(f"{cache_key(flow)}*"))
def get_export_url(self, flow: Flow) -> str:
"""Get export URL for flow"""
return reverse("authentik_api:flow-export", kwargs={"slug": flow.slug})
class Meta:
model = Flow
@ -64,6 +71,7 @@ class FlowSerializer(ModelSerializer):
"cache_count",
"policy_engine_mode",
"compatibility_mode",
"export_url",
]
extra_kwargs = {
"background": {"read_only": True},
@ -94,7 +102,7 @@ class DiagramElement:
return f"{self.identifier}=>{self.type}: {self.rest}"
class FlowViewSet(ModelViewSet):
class FlowViewSet(UsedByMixin, ModelViewSet):
"""Flow Viewset"""
queryset = Flow.objects.all()

View File

@ -11,6 +11,7 @@ from rest_framework.serializers import ModelSerializer, SerializerMethodField
from rest_framework.viewsets import GenericViewSet
from structlog.stdlib import get_logger
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import MetaNameSerializer, TypeCreateSerializer
from authentik.core.types import UserSettingSerializer
from authentik.flows.api.flows import FlowSerializer
@ -49,6 +50,7 @@ class StageSerializer(ModelSerializer, MetaNameSerializer):
class StageViewSet(
mixins.RetrieveModelMixin,
mixins.DestroyModelMixin,
UsedByMixin,
mixins.ListModelMixin,
GenericViewSet,
):
@ -91,10 +93,10 @@ class StageViewSet(
if not user_settings:
continue
user_settings.initial_data["object_uid"] = str(stage.pk)
if hasattr(stage, "configure_url"):
if hasattr(stage, "configure_flow"):
user_settings.initial_data["configure_url"] = reverse(
"authentik_flows:configure",
kwargs={"stage_uuid": stage.uuid.hex},
kwargs={"stage_uuid": stage.pk},
)
if not user_settings.is_valid():
LOGGER.warning(user_settings.errors)

View File

@ -72,7 +72,7 @@ class Stage(SerializerModel):
def __str__(self):
if hasattr(self, "__in_memory_type"):
return f"In-memory Stage {getattr(self, '__in_memory_type')}"
return self.name
return f"Stage {self.name}"
def in_memory_stage(view: Type["StageView"]) -> Stage:
@ -212,7 +212,7 @@ class FlowStageBinding(SerializerModel, PolicyBindingModel):
return FlowStageBindingSerializer
def __str__(self) -> str:
return f"{self.target} #{self.order}"
return f"Flow-stage binding #{self.order} to {self.target}"
class Meta:

View File

@ -50,14 +50,17 @@ class StageView(View):
self.executor = executor
super().__init__(**kwargs)
def get_pending_user(self) -> User:
def get_pending_user(self, for_display=False) -> User:
"""Either show the matched User object or show what the user entered,
based on what the earlier stage (mostly IdentificationStage) set.
_USER_IDENTIFIER overrides the first User, as PENDING_USER is used for
other things besides the form display.
If no user is pending, returns request.user"""
if PLAN_CONTEXT_PENDING_USER_IDENTIFIER in self.executor.plan.context:
if (
PLAN_CONTEXT_PENDING_USER_IDENTIFIER in self.executor.plan.context
and for_display
):
return User(
username=self.executor.plan.context.get(
PLAN_CONTEXT_PENDING_USER_IDENTIFIER
@ -109,7 +112,7 @@ class ChallengeStageView(StageView):
# If there's a pending user, update the `username` field
# this field is only used by password managers.
# If there's no user set, an error is raised later.
if user := self.get_pending_user():
if user := self.get_pending_user(for_display=True):
challenge.initial_data["pending_user"] = user.username
challenge.initial_data["pending_user_avatar"] = DEFAULT_AVATAR
if not isinstance(user, AnonymousUser):

View File

@ -511,4 +511,4 @@ class TestFlowExecutor(TestCase):
executor.flow = flow
stage_view = StageView(executor)
self.assertEqual(ident, stage_view.get_pending_user().username)
self.assertEqual(ident, stage_view.get_pending_user(for_display=True).username)

View File

@ -11,6 +11,7 @@ from rest_framework.serializers import JSONField, ModelSerializer, ValidationErr
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.providers import ProviderSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import PassiveSerializer, is_dict
from authentik.core.models import Provider
from authentik.outposts.api.service_connections import ServiceConnectionSerializer
@ -95,7 +96,7 @@ class OutpostHealthSerializer(PassiveSerializer):
version_outdated = BooleanField(read_only=True)
class OutpostViewSet(ModelViewSet):
class OutpostViewSet(UsedByMixin, ModelViewSet):
"""Outpost Viewset"""
queryset = Outpost.objects.all()

View File

@ -14,6 +14,7 @@ from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import GenericViewSet, ModelViewSet
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import (
MetaNameSerializer,
PassiveSerializer,
@ -55,6 +56,7 @@ class ServiceConnectionStateSerializer(PassiveSerializer):
class ServiceConnectionViewSet(
mixins.RetrieveModelMixin,
mixins.DestroyModelMixin,
UsedByMixin,
mixins.ListModelMixin,
GenericViewSet,
):
@ -105,7 +107,7 @@ class DockerServiceConnectionSerializer(ServiceConnectionSerializer):
]
class DockerServiceConnectionViewSet(ModelViewSet):
class DockerServiceConnectionViewSet(UsedByMixin, ModelViewSet):
"""DockerServiceConnection Viewset"""
queryset = DockerServiceConnection.objects.all()
@ -139,7 +141,7 @@ class KubernetesServiceConnectionSerializer(ServiceConnectionSerializer):
fields = ServiceConnectionSerializer.Meta.fields + ["kubeconfig"]
class KubernetesServiceConnectionViewSet(ModelViewSet):
class KubernetesServiceConnectionViewSet(UsedByMixin, ModelViewSet):
"""KubernetesServiceConnection Viewset"""
queryset = KubernetesServiceConnection.objects.all()

View File

@ -11,6 +11,7 @@ from rest_framework.viewsets import ModelViewSet
from structlog.stdlib import get_logger
from authentik.core.api.groups import GroupSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.users import UserSerializer
from authentik.policies.api.policies import PolicySerializer
from authentik.policies.models import PolicyBinding, PolicyBindingModel
@ -99,7 +100,7 @@ class PolicyBindingSerializer(ModelSerializer):
return data
class PolicyBindingViewSet(ModelViewSet):
class PolicyBindingViewSet(UsedByMixin, ModelViewSet):
"""PolicyBinding Viewset"""
queryset = (

View File

@ -14,6 +14,7 @@ from structlog.stdlib import get_logger
from authentik.api.decorators import permission_required
from authentik.core.api.applications import user_app_cache_key
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import (
CacheSerializer,
MetaNameSerializer,
@ -79,6 +80,7 @@ class PolicySerializer(ModelSerializer, MetaNameSerializer):
class PolicyViewSet(
mixins.RetrieveModelMixin,
mixins.DestroyModelMixin,
UsedByMixin,
mixins.ListModelMixin,
GenericViewSet,
):

View File

@ -1,6 +1,7 @@
"""Dummy Policy API Views"""
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.used_by import UsedByMixin
from authentik.policies.api.policies import PolicySerializer
from authentik.policies.dummy.models import DummyPolicy
@ -13,7 +14,7 @@ class DummyPolicySerializer(PolicySerializer):
fields = PolicySerializer.Meta.fields + ["result", "wait_min", "wait_max"]
class DummyPolicyViewSet(ModelViewSet):
class DummyPolicyViewSet(UsedByMixin, ModelViewSet):
"""Dummy Viewset"""
queryset = DummyPolicy.objects.all()

View File

@ -1,6 +1,7 @@
"""Event Matcher Policy API"""
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.used_by import UsedByMixin
from authentik.policies.api.policies import PolicySerializer
from authentik.policies.event_matcher.models import EventMatcherPolicy
@ -17,7 +18,7 @@ class EventMatcherPolicySerializer(PolicySerializer):
]
class EventMatcherPolicyViewSet(ModelViewSet):
class EventMatcherPolicyViewSet(UsedByMixin, ModelViewSet):
"""Event Matcher Policy Viewset"""
queryset = EventMatcherPolicy.objects.all()

View File

@ -1,6 +1,7 @@
"""Password Expiry Policy API Views"""
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.used_by import UsedByMixin
from authentik.policies.api.policies import PolicySerializer
from authentik.policies.expiry.models import PasswordExpiryPolicy
@ -13,7 +14,7 @@ class PasswordExpiryPolicySerializer(PolicySerializer):
fields = PolicySerializer.Meta.fields + ["days", "deny_only"]
class PasswordExpiryPolicyViewSet(ModelViewSet):
class PasswordExpiryPolicyViewSet(UsedByMixin, ModelViewSet):
"""Password Expiry Viewset"""
queryset = PasswordExpiryPolicy.objects.all()

View File

@ -1,6 +1,7 @@
"""Expression Policy API"""
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.used_by import UsedByMixin
from authentik.policies.api.policies import PolicySerializer
from authentik.policies.expression.evaluator import PolicyEvaluator
from authentik.policies.expression.models import ExpressionPolicy
@ -20,7 +21,7 @@ class ExpressionPolicySerializer(PolicySerializer):
fields = PolicySerializer.Meta.fields + ["expression"]
class ExpressionPolicyViewSet(ModelViewSet):
class ExpressionPolicyViewSet(UsedByMixin, ModelViewSet):
"""Source Viewset"""
queryset = ExpressionPolicy.objects.all()

View File

@ -1,6 +1,7 @@
"""Source API Views"""
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.used_by import UsedByMixin
from authentik.policies.api.policies import PolicySerializer
from authentik.policies.hibp.models import HaveIBeenPwendPolicy
@ -13,7 +14,7 @@ class HaveIBeenPwendPolicySerializer(PolicySerializer):
fields = PolicySerializer.Meta.fields + ["password_field", "allowed_count"]
class HaveIBeenPwendPolicyViewSet(ModelViewSet):
class HaveIBeenPwendPolicyViewSet(UsedByMixin, ModelViewSet):
"""Source Viewset"""
queryset = HaveIBeenPwendPolicy.objects.all()

View File

@ -1,6 +1,7 @@
"""Password Policy API Views"""
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.used_by import UsedByMixin
from authentik.policies.api.policies import PolicySerializer
from authentik.policies.password.models import PasswordPolicy
@ -21,7 +22,7 @@ class PasswordPolicySerializer(PolicySerializer):
]
class PasswordPolicyViewSet(ModelViewSet):
class PasswordPolicyViewSet(UsedByMixin, ModelViewSet):
"""Password Policy Viewset"""
queryset = PasswordPolicy.objects.all()

View File

@ -3,6 +3,7 @@ from rest_framework import mixins
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import GenericViewSet, ModelViewSet
from authentik.core.api.used_by import UsedByMixin
from authentik.policies.api.policies import PolicySerializer
from authentik.policies.reputation.models import (
IPReputation,
@ -23,7 +24,7 @@ class ReputationPolicySerializer(PolicySerializer):
]
class ReputationPolicyViewSet(ModelViewSet):
class ReputationPolicyViewSet(UsedByMixin, ModelViewSet):
"""Reputation Policy Viewset"""
queryset = ReputationPolicy.objects.all()
@ -46,6 +47,7 @@ class IPReputationSerializer(ModelSerializer):
class IPReputationViewSet(
mixins.RetrieveModelMixin,
mixins.DestroyModelMixin,
UsedByMixin,
mixins.ListModelMixin,
GenericViewSet,
):
@ -74,6 +76,7 @@ class UserReputationSerializer(ModelSerializer):
class UserReputationViewSet(
mixins.RetrieveModelMixin,
mixins.DestroyModelMixin,
UsedByMixin,
mixins.ListModelMixin,
GenericViewSet,
):

View File

@ -4,6 +4,7 @@ from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
from authentik.core.api.providers import ProviderSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.providers.ldap.models import LDAPProvider
@ -19,7 +20,7 @@ class LDAPProviderSerializer(ProviderSerializer):
]
class LDAPProviderViewSet(ModelViewSet):
class LDAPProviderViewSet(UsedByMixin, ModelViewSet):
"""LDAPProvider Viewset"""
queryset = LDAPProvider.objects.all()

View File

@ -11,6 +11,7 @@ from rest_framework.serializers import ValidationError
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.providers import ProviderSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import PassiveSerializer
from authentik.core.models import Provider
from authentik.providers.oauth2.models import JWTAlgorithms, OAuth2Provider
@ -61,7 +62,7 @@ class OAuth2ProviderSetupURLs(PassiveSerializer):
logout = ReadOnlyField()
class OAuth2ProviderViewSet(ModelViewSet):
class OAuth2ProviderViewSet(UsedByMixin, ModelViewSet):
"""OAuth2Provider Viewset"""
queryset = OAuth2Provider.objects.all()

View File

@ -2,6 +2,7 @@
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.propertymappings import PropertyMappingSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.providers.oauth2.models import ScopeMapping
@ -17,7 +18,7 @@ class ScopeMappingSerializer(PropertyMappingSerializer):
]
class ScopeMappingViewSet(ModelViewSet):
class ScopeMappingViewSet(UsedByMixin, ModelViewSet):
"""ScopeMapping Viewset"""
queryset = ScopeMapping.objects.all()

View File

@ -10,6 +10,7 @@ from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import GenericViewSet
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.users import UserSerializer
from authentik.core.api.utils import MetaNameSerializer
from authentik.providers.oauth2.api.provider import OAuth2ProviderSerializer
@ -57,6 +58,7 @@ class RefreshTokenModelSerializer(ExpiringBaseGrantModelSerializer):
class AuthorizationCodeViewSet(
mixins.RetrieveModelMixin,
mixins.DestroyModelMixin,
UsedByMixin,
mixins.ListModelMixin,
GenericViewSet,
):
@ -82,6 +84,7 @@ class AuthorizationCodeViewSet(
class RefreshTokenViewSet(
mixins.RetrieveModelMixin,
mixins.DestroyModelMixin,
UsedByMixin,
mixins.ListModelMixin,
GenericViewSet,
):

View File

@ -0,0 +1,26 @@
# Generated by Django 3.2.3 on 2021-06-09 21:52
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_crypto", "0002_create_self_signed_kp"),
("authentik_providers_oauth2", "0013_alter_authorizationcode_nonce"),
]
operations = [
migrations.AlterField(
model_name="oauth2provider",
name="rsa_key",
field=models.ForeignKey(
help_text="Key used to sign the tokens. Only required when JWT Algorithm is set to RS256.",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="authentik_crypto.certificatekeypair",
verbose_name="RSA Key",
),
),
]

View File

@ -215,8 +215,7 @@ class OAuth2Provider(Provider):
rsa_key = models.ForeignKey(
CertificateKeyPair,
verbose_name=_("RSA Key"),
on_delete=models.CASCADE,
blank=True,
on_delete=models.SET_NULL,
null=True,
help_text=_(
"Key used to sign the tokens. Only required when JWT Algorithm is set to RS256."

View File

@ -8,6 +8,7 @@ from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
from authentik.core.api.providers import ProviderSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import PassiveSerializer
from authentik.providers.oauth2.views.provider import ProviderInfoView
from authentik.providers.proxy.models import ProxyMode, ProxyProvider
@ -76,7 +77,7 @@ class ProxyProviderSerializer(ProviderSerializer):
]
class ProxyProviderViewSet(ModelViewSet):
class ProxyProviderViewSet(UsedByMixin, ModelViewSet):
"""ProxyProvider Viewset"""
queryset = ProxyProvider.objects.all()

View File

@ -167,3 +167,4 @@ class ProxyProvider(OutpostModel, OAuth2Provider):
verbose_name = _("Proxy Provider")
verbose_name_plural = _("Proxy Providers")
authentik_used_by_shadows = ["authentik_providers_oauth2.oauth2provider"]

View File

@ -4,11 +4,17 @@ from xml.etree.ElementTree import ParseError # nosec
from defusedxml.ElementTree import fromstring
from django.http.response import HttpResponse
from django.shortcuts import get_object_or_404
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema
from rest_framework.decorators import action
from rest_framework.fields import CharField, FileField, ReadOnlyField
from rest_framework.fields import (
CharField,
FileField,
ReadOnlyField,
SerializerMethodField,
)
from rest_framework.parsers import MultiPartParser
from rest_framework.permissions import AllowAny
from rest_framework.relations import SlugRelatedField
@ -21,6 +27,7 @@ from structlog.stdlib import get_logger
from authentik.api.decorators import permission_required
from authentik.core.api.propertymappings import PropertyMappingSerializer
from authentik.core.api.providers import ProviderSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import PassiveSerializer
from authentik.core.models import Provider
from authentik.flows.models import Flow, FlowDesignation
@ -36,6 +43,15 @@ LOGGER = get_logger()
class SAMLProviderSerializer(ProviderSerializer):
"""SAMLProvider Serializer"""
metadata_download_url = SerializerMethodField()
def get_metadata_download_url(self, instance: SAMLProvider) -> str:
"""Get metadata download URL"""
return (
reverse("authentik_api:samlprovider-metadata", kwargs={"pk": instance.pk})
+ "?download"
)
class Meta:
model = SAMLProvider
@ -53,6 +69,7 @@ class SAMLProviderSerializer(ProviderSerializer):
"signing_kp",
"verification_kp",
"sp_binding",
"metadata_download_url",
]
@ -75,7 +92,7 @@ class SAMLProviderImportSerializer(PassiveSerializer):
file = FileField()
class SAMLProviderViewSet(ModelViewSet):
class SAMLProviderViewSet(UsedByMixin, ModelViewSet):
"""SAMLProvider Viewset"""
queryset = SAMLProvider.objects.all()
@ -166,7 +183,7 @@ class SAMLPropertyMappingSerializer(PropertyMappingSerializer):
]
class SAMLPropertyMappingViewSet(ModelViewSet):
class SAMLPropertyMappingViewSet(UsedByMixin, ModelViewSet):
"""SAMLPropertyMapping Viewset"""
queryset = SAMLPropertyMapping.objects.all()

View File

@ -10,6 +10,7 @@ from rest_framework.viewsets import ModelViewSet
from authentik.admin.api.tasks import TaskSerializer
from authentik.core.api.propertymappings import PropertyMappingSerializer
from authentik.core.api.sources import SourceSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.events.monitored_tasks import TaskInfo
from authentik.sources.ldap.models import LDAPPropertyMapping, LDAPSource
@ -41,7 +42,7 @@ class LDAPSourceSerializer(SourceSerializer):
extra_kwargs = {"bind_password": {"write_only": True}}
class LDAPSourceViewSet(ModelViewSet):
class LDAPSourceViewSet(UsedByMixin, ModelViewSet):
"""LDAP Source Viewset"""
queryset = LDAPSource.objects.all()
@ -75,7 +76,7 @@ class LDAPPropertyMappingSerializer(PropertyMappingSerializer):
]
class LDAPPropertyMappingViewSet(ModelViewSet):
class LDAPPropertyMappingViewSet(UsedByMixin, ModelViewSet):
"""LDAP PropertyMapping Viewset"""
queryset = LDAPPropertyMapping.objects.all()

View File

@ -9,6 +9,7 @@ from rest_framework.serializers import ValidationError
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.sources import SourceSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import PassiveSerializer
from authentik.sources.oauth.models import OAuthSource
from authentik.sources.oauth.types.manager import MANAGER
@ -78,7 +79,7 @@ class OAuthSourceSerializer(SourceSerializer):
extra_kwargs = {"consumer_secret": {"write_only": True}}
class OAuthSourceViewSet(ModelViewSet):
class OAuthSourceViewSet(UsedByMixin, ModelViewSet):
"""Source Viewset"""
queryset = OAuthSource.objects.all()

View File

@ -6,6 +6,7 @@ from rest_framework.viewsets import GenericViewSet
from authentik.api.authorization import OwnerFilter, OwnerPermissions
from authentik.core.api.sources import SourceSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.sources.oauth.models import UserOAuthSourceConnection
@ -26,6 +27,7 @@ class UserOAuthSourceConnectionViewSet(
mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
UsedByMixin,
mixins.ListModelMixin,
GenericViewSet,
):

View File

@ -14,6 +14,7 @@ from structlog.stdlib import get_logger
from authentik.api.decorators import permission_required
from authentik.core.api.sources import SourceSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import PassiveSerializer
from authentik.flows.challenge import RedirectChallenge
from authentik.flows.views import to_stage_response
@ -42,7 +43,7 @@ class PlexTokenRedeemSerializer(PassiveSerializer):
plex_token = CharField()
class PlexSourceViewSet(ModelViewSet):
class PlexSourceViewSet(UsedByMixin, ModelViewSet):
"""Plex source Viewset"""
queryset = PlexSource.objects.all()

View File

@ -7,6 +7,7 @@ from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.sources import SourceSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.providers.saml.api import SAMLMetadataSerializer
from authentik.sources.saml.models import SAMLSource
from authentik.sources.saml.processors.metadata import MetadataProcessor
@ -33,7 +34,7 @@ class SAMLSourceSerializer(SourceSerializer):
]
class SAMLSourceViewSet(ModelViewSet):
class SAMLSourceViewSet(UsedByMixin, ModelViewSet):
"""SAMLSource Viewset"""
queryset = SAMLSource.objects.all()

View File

@ -12,6 +12,7 @@ from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import GenericViewSet, ModelViewSet, ReadOnlyModelViewSet
from authentik.api.authorization import OwnerFilter, OwnerPermissions
from authentik.core.api.used_by import UsedByMixin
from authentik.flows.api.stages import StageSerializer
from authentik.stages.authenticator_duo.models import AuthenticatorDuoStage, DuoDevice
from authentik.stages.authenticator_duo.stage import (
@ -37,7 +38,7 @@ class AuthenticatorDuoStageSerializer(StageSerializer):
}
class AuthenticatorDuoStageViewSet(ModelViewSet):
class AuthenticatorDuoStageViewSet(UsedByMixin, ModelViewSet):
"""AuthenticatorDuoStage Viewset"""
queryset = AuthenticatorDuoStage.objects.all()
@ -78,6 +79,7 @@ class DuoDeviceViewSet(
mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
UsedByMixin,
mixins.ListModelMixin,
GenericViewSet,
):

View File

@ -8,6 +8,7 @@ from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import GenericViewSet, ModelViewSet, ReadOnlyModelViewSet
from authentik.api.authorization import OwnerFilter, OwnerPermissions
from authentik.core.api.used_by import UsedByMixin
from authentik.flows.api.stages import StageSerializer
from authentik.stages.authenticator_static.models import AuthenticatorStaticStage
@ -21,7 +22,7 @@ class AuthenticatorStaticStageSerializer(StageSerializer):
fields = StageSerializer.Meta.fields + ["configure_flow", "token_count"]
class AuthenticatorStaticStageViewSet(ModelViewSet):
class AuthenticatorStaticStageViewSet(UsedByMixin, ModelViewSet):
"""AuthenticatorStaticStage Viewset"""
queryset = AuthenticatorStaticStage.objects.all()
@ -52,6 +53,7 @@ class StaticDeviceViewSet(
mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
UsedByMixin,
mixins.ListModelMixin,
GenericViewSet,
):

View File

@ -8,6 +8,7 @@ from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import GenericViewSet, ModelViewSet, ReadOnlyModelViewSet
from authentik.api.authorization import OwnerFilter, OwnerPermissions
from authentik.core.api.used_by import UsedByMixin
from authentik.flows.api.stages import StageSerializer
from authentik.stages.authenticator_totp.models import AuthenticatorTOTPStage
@ -21,7 +22,7 @@ class AuthenticatorTOTPStageSerializer(StageSerializer):
fields = StageSerializer.Meta.fields + ["configure_flow", "digits"]
class AuthenticatorTOTPStageViewSet(ModelViewSet):
class AuthenticatorTOTPStageViewSet(UsedByMixin, ModelViewSet):
"""AuthenticatorTOTPStage Viewset"""
queryset = AuthenticatorTOTPStage.objects.all()
@ -45,6 +46,7 @@ class TOTPDeviceViewSet(
mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
UsedByMixin,
mixins.ListModelMixin,
GenericViewSet,
):

View File

@ -2,6 +2,7 @@
from rest_framework.serializers import ValidationError
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.used_by import UsedByMixin
from authentik.flows.api.stages import StageSerializer
from authentik.flows.models import NotConfiguredAction
from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage
@ -32,7 +33,7 @@ class AuthenticatorValidateStageSerializer(StageSerializer):
]
class AuthenticatorValidateStageViewSet(ModelViewSet):
class AuthenticatorValidateStageViewSet(UsedByMixin, ModelViewSet):
"""AuthenticatorValidateStage Viewset"""
queryset = AuthenticatorValidateStage.objects.all()

View File

@ -91,6 +91,7 @@ class AuthenticatorValidateStageView(ChallengeStageView):
"""Get a list of all device challenges applicable for the current stage"""
challenges = []
user_devices = devices_for_user(self.get_pending_user())
LOGGER.debug("Got devices for user", devices=user_devices)
# static and totp are only shown once
# since their challenges are device-independant
@ -101,6 +102,7 @@ class AuthenticatorValidateStageView(ChallengeStageView):
for device in user_devices:
device_class = device.__class__.__name__.lower().replace("device", "")
if device_class not in stage.device_classes:
LOGGER.debug("device class not allowed", device_class=device_class)
continue
# Ensure only classes in PER_DEVICE_CLASSES are returned per device
# otherwise only return a single challenge
@ -108,15 +110,16 @@ class AuthenticatorValidateStageView(ChallengeStageView):
continue
if device_class not in seen_classes:
seen_classes.append(device_class)
challenges.append(
DeviceChallenge(
data={
"device_class": device_class,
"device_uid": device.pk,
"challenge": get_challenge_for_device(self.request, device),
}
).initial_data
challenge = DeviceChallenge(
data={
"device_class": device_class,
"device_uid": device.pk,
"challenge": get_challenge_for_device(self.request, device),
}
)
challenge.is_valid()
challenges.append(challenge.data)
LOGGER.debug("adding challenge for device", challenge=challenge)
return challenges
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:

View File

@ -7,6 +7,7 @@ from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import GenericViewSet, ModelViewSet, ReadOnlyModelViewSet
from authentik.api.authorization import OwnerFilter, OwnerPermissions
from authentik.core.api.used_by import UsedByMixin
from authentik.flows.api.stages import StageSerializer
from authentik.stages.authenticator_webauthn.models import (
AuthenticateWebAuthnStage,
@ -23,7 +24,7 @@ class AuthenticateWebAuthnStageSerializer(StageSerializer):
fields = StageSerializer.Meta.fields + ["configure_flow"]
class AuthenticateWebAuthnStageViewSet(ModelViewSet):
class AuthenticateWebAuthnStageViewSet(UsedByMixin, ModelViewSet):
"""AuthenticateWebAuthnStage Viewset"""
queryset = AuthenticateWebAuthnStage.objects.all()
@ -44,6 +45,7 @@ class WebAuthnDeviceViewSet(
mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
UsedByMixin,
mixins.ListModelMixin,
GenericViewSet,
):

View File

@ -1,6 +1,7 @@
"""CaptchaStage API Views"""
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.used_by import UsedByMixin
from authentik.flows.api.stages import StageSerializer
from authentik.stages.captcha.models import CaptchaStage
@ -15,7 +16,7 @@ class CaptchaStageSerializer(StageSerializer):
extra_kwargs = {"private_key": {"write_only": True}}
class CaptchaStageViewSet(ModelViewSet):
class CaptchaStageViewSet(UsedByMixin, ModelViewSet):
"""CaptchaStage Viewset"""
queryset = CaptchaStage.objects.all()

View File

@ -6,6 +6,7 @@ from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.viewsets import GenericViewSet, ModelViewSet
from authentik.core.api.applications import ApplicationSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.users import UserSerializer
from authentik.flows.api.stages import StageSerializer
from authentik.stages.consent.models import ConsentStage, UserConsent
@ -20,7 +21,7 @@ class ConsentStageSerializer(StageSerializer):
fields = StageSerializer.Meta.fields + ["mode", "consent_expire_in"]
class ConsentStageViewSet(ModelViewSet):
class ConsentStageViewSet(UsedByMixin, ModelViewSet):
"""ConsentStage Viewset"""
queryset = ConsentStage.objects.all()
@ -42,6 +43,7 @@ class UserConsentSerializer(StageSerializer):
class UserConsentViewSet(
mixins.RetrieveModelMixin,
mixins.DestroyModelMixin,
UsedByMixin,
mixins.ListModelMixin,
GenericViewSet,
):

View File

@ -1,6 +1,7 @@
"""deny Stage API Views"""
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.used_by import UsedByMixin
from authentik.flows.api.stages import StageSerializer
from authentik.stages.deny.models import DenyStage
@ -14,7 +15,7 @@ class DenyStageSerializer(StageSerializer):
fields = StageSerializer.Meta.fields
class DenyStageViewSet(ModelViewSet):
class DenyStageViewSet(UsedByMixin, ModelViewSet):
"""DenyStage Viewset"""
queryset = DenyStage.objects.all()

View File

@ -1,6 +1,7 @@
"""DummyStage API Views"""
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.used_by import UsedByMixin
from authentik.flows.api.stages import StageSerializer
from authentik.stages.dummy.models import DummyStage
@ -14,7 +15,7 @@ class DummyStageSerializer(StageSerializer):
fields = StageSerializer.Meta.fields
class DummyStageViewSet(ModelViewSet):
class DummyStageViewSet(UsedByMixin, ModelViewSet):
"""DummyStage Viewset"""
queryset = DummyStage.objects.all()

View File

@ -6,6 +6,7 @@ from rest_framework.response import Response
from rest_framework.serializers import ValidationError
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import TypeCreateSerializer
from authentik.flows.api.stages import StageSerializer
from authentik.stages.email.models import EmailStage, get_template_choices
@ -46,7 +47,7 @@ class EmailStageSerializer(StageSerializer):
extra_kwargs = {"password": {"write_only": True}}
class EmailStageViewSet(ModelViewSet):
class EmailStageViewSet(UsedByMixin, ModelViewSet):
"""EmailStage Viewset"""
queryset = EmailStage.objects.all()

View File

@ -1,6 +1,7 @@
"""Identification Stage API Views"""
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.used_by import UsedByMixin
from authentik.flows.api.stages import StageSerializer
from authentik.stages.identification.models import IdentificationStage
@ -22,7 +23,7 @@ class IdentificationStageSerializer(StageSerializer):
]
class IdentificationStageViewSet(ModelViewSet):
class IdentificationStageViewSet(UsedByMixin, ModelViewSet):
"""IdentificationStage Viewset"""
queryset = IdentificationStage.objects.all()

View File

@ -3,6 +3,7 @@ from rest_framework.fields import JSONField
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.users import UserSerializer
from authentik.core.api.utils import is_dict
from authentik.flows.api.stages import StageSerializer
@ -20,7 +21,7 @@ class InvitationStageSerializer(StageSerializer):
]
class InvitationStageViewSet(ModelViewSet):
class InvitationStageViewSet(UsedByMixin, ModelViewSet):
"""InvitationStage Viewset"""
queryset = InvitationStage.objects.all()
@ -45,7 +46,7 @@ class InvitationSerializer(ModelSerializer):
]
class InvitationViewSet(ModelViewSet):
class InvitationViewSet(UsedByMixin, ModelViewSet):
"""Invitation Viewset"""
queryset = Invitation.objects.all()

View File

@ -1,6 +1,7 @@
"""PasswordStage API Views"""
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.used_by import UsedByMixin
from authentik.flows.api.stages import StageSerializer
from authentik.stages.password.models import PasswordStage
@ -18,7 +19,7 @@ class PasswordStageSerializer(StageSerializer):
]
class PasswordStageViewSet(ModelViewSet):
class PasswordStageViewSet(UsedByMixin, ModelViewSet):
"""PasswordStage Viewset"""
queryset = PasswordStage.objects.all()

View File

@ -3,6 +3,7 @@ from rest_framework.serializers import CharField, ModelSerializer
from rest_framework.validators import UniqueValidator
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.used_by import UsedByMixin
from authentik.flows.api.stages import StageSerializer
from authentik.stages.prompt.models import Prompt, PromptStage
@ -21,7 +22,7 @@ class PromptStageSerializer(StageSerializer):
]
class PromptStageViewSet(ModelViewSet):
class PromptStageViewSet(UsedByMixin, ModelViewSet):
"""PromptStage Viewset"""
queryset = PromptStage.objects.all()
@ -48,7 +49,7 @@ class PromptSerializer(ModelSerializer):
]
class PromptViewSet(ModelViewSet):
class PromptViewSet(UsedByMixin, ModelViewSet):
"""Prompt Viewset"""
queryset = Prompt.objects.all().prefetch_related("promptstage_set")

View File

@ -1,6 +1,7 @@
"""User Delete Stage API Views"""
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.used_by import UsedByMixin
from authentik.flows.api.stages import StageSerializer
from authentik.stages.user_delete.models import UserDeleteStage
@ -14,7 +15,7 @@ class UserDeleteStageSerializer(StageSerializer):
fields = StageSerializer.Meta.fields
class UserDeleteStageViewSet(ModelViewSet):
class UserDeleteStageViewSet(UsedByMixin, ModelViewSet):
"""UserDeleteStage Viewset"""
queryset = UserDeleteStage.objects.all()

View File

@ -1,6 +1,7 @@
"""Login Stage API Views"""
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.used_by import UsedByMixin
from authentik.flows.api.stages import StageSerializer
from authentik.stages.user_login.models import UserLoginStage
@ -16,7 +17,7 @@ class UserLoginStageSerializer(StageSerializer):
]
class UserLoginStageViewSet(ModelViewSet):
class UserLoginStageViewSet(UsedByMixin, ModelViewSet):
"""UserLoginStage Viewset"""
queryset = UserLoginStage.objects.all()

View File

@ -1,6 +1,7 @@
"""Logout Stage API Views"""
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.used_by import UsedByMixin
from authentik.flows.api.stages import StageSerializer
from authentik.stages.user_logout.models import UserLogoutStage
@ -14,7 +15,7 @@ class UserLogoutStageSerializer(StageSerializer):
fields = StageSerializer.Meta.fields
class UserLogoutStageViewSet(ModelViewSet):
class UserLogoutStageViewSet(UsedByMixin, ModelViewSet):
"""UserLogoutStage Viewset"""
queryset = UserLogoutStage.objects.all()

View File

@ -1,6 +1,7 @@
"""User Write Stage API Views"""
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.used_by import UsedByMixin
from authentik.flows.api.stages import StageSerializer
from authentik.stages.user_write.models import UserWriteStage
@ -14,7 +15,7 @@ class UserWriteStageSerializer(StageSerializer):
fields = StageSerializer.Meta.fields
class UserWriteStageViewSet(ModelViewSet):
class UserWriteStageViewSet(UsedByMixin, ModelViewSet):
"""UserWriteStage Viewset"""
queryset = UserWriteStage.objects.all()

View File

@ -8,6 +8,7 @@ from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import PassiveSerializer
from authentik.lib.config import CONFIG
from authentik.tenants.models import Tenant
@ -56,7 +57,7 @@ class CurrentTenantSerializer(PassiveSerializer):
flow_unenrollment = CharField(source="flow_unenrollment.slug", required=False)
class TenantViewSet(ModelViewSet):
class TenantViewSet(UsedByMixin, ModelViewSet):
"""Tenant Viewset"""
queryset = Tenant.objects.all()

View File

@ -41,7 +41,9 @@ class Tenant(models.Model):
)
def __str__(self) -> str:
return self.domain
if self.default:
return "Default tenant"
return f"Tenant {self.domain}"
class Meta:

View File

@ -21,7 +21,7 @@ services:
networks:
- internal
server:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2021.6.1-rc1}
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2021.6.1-rc4}
restart: unless-stopped
command: server
environment:
@ -52,7 +52,7 @@ services:
- "0.0.0.0:9000:9000"
- "0.0.0.0:9443:9443"
worker:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2021.6.1-rc1}
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2021.6.1-rc4}
restart: unless-stopped
command: worker
networks:

View File

@ -1,3 +1,3 @@
package constants
const VERSION = "2021.6.1-rc1"
const VERSION = "2021.6.1-rc4"

View File

@ -9,16 +9,18 @@ function check_if_root {
return
fi
SOCKET="/var/run/docker.sock"
GROUP="authentik"
if [[ -e "$SOCKET" ]]; then
# Get group ID of the docker socket, so we can create a matching group and
# add ourselves to it
DOCKER_GID=$(stat -c '%g' $SOCKET)
getent group $DOCKER_GID || groupadd -f -g $DOCKER_GID docker
usermod -a -G $DOCKER_GID authentik
GROUP="authentik:docker"
fi
# Fix permissions of backups and media
chown -R authentik:authentik /media /backups
chpst -u authentik:authentik:docker env HOME=/authentik $1
chpst -u authentik:$GROUP env HOME=/authentik $1
}
if [[ "$1" == "server" ]]; then
@ -32,6 +34,11 @@ elif [[ "$1" == "restore" ]]; then
python -m manage dbrestore ${@:2}
elif [[ "$1" == "bash" ]]; then
/bin/bash
elif [[ "$1" == "test" ]]; then
pip install --no-cache -r requirements-dev.txt
touch /unittest.xml
chown authentik:authentik /unittest.xml
check_if_root "python -m manage test authentik"
else
python -m manage "$@"
fi

View File

@ -78,6 +78,7 @@ stages:
tags: |
gh-$(branchName)
gh-$(branchName)-$(timestamp)
gh-$(Build.SourceVersion)
arguments: '--build-arg GIT_BUILD_HASH=$(Build.SourceVersion)'
- task: Docker@2
inputs:
@ -87,6 +88,7 @@ stages:
tags: |
gh-$(branchName)
gh-$(branchName)-$(timestamp)
gh-$(Build.SourceVersion)
- job: ldap_build_docker
pool:
vmImage: 'ubuntu-latest'
@ -109,6 +111,7 @@ stages:
tags: |
gh-$(branchName)
gh-$(branchName)-$(timestamp)
gh-$(Build.SourceVersion)
arguments: '--build-arg GIT_BUILD_HASH=$(Build.SourceVersion)'
- task: Docker@2
inputs:
@ -118,3 +121,4 @@ stages:
tags: |
gh-$(branchName)
gh-$(branchName)-$(timestamp)
gh-$(Build.SourceVersion)

View File

@ -5,7 +5,7 @@ import (
"os"
)
const VERSION = "2021.6.1-rc1"
const VERSION = "2021.6.1-rc4"
func BUILD() string {
build := os.Getenv("GIT_BUILD_HASH")

View File

@ -9,6 +9,7 @@ force_grid_wrap = 0
use_parentheses = true
line_length = 88
src_paths = ["authentik", "tests", "lifecycle"]
force_to_top = "*"
[tool.coverage.run]
source = ["authentik"]

2180
schema.yml

File diff suppressed because it is too large Load Diff

2718
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -38,11 +38,11 @@
]
},
"dependencies": {
"@babel/core": "^7.14.3",
"@babel/plugin-proposal-decorators": "^7.14.2",
"@babel/plugin-transform-runtime": "^7.14.3",
"@babel/preset-env": "^7.14.4",
"@babel/preset-typescript": "^7.13.0",
"@babel/core": "^7.14.5",
"@babel/plugin-proposal-decorators": "^7.14.5",
"@babel/plugin-transform-runtime": "^7.14.5",
"@babel/preset-env": "^7.14.5",
"@babel/preset-typescript": "^7.14.5",
"@fortawesome/fontawesome-free": "^5.15.3",
"@lingui/cli": "^3.10.2",
"@lingui/core": "^3.10.2",
@ -71,7 +71,7 @@
"eslint": "^7.28.0",
"eslint-config-google": "^0.14.0",
"eslint-plugin-custom-elements": "0.0.2",
"eslint-plugin-lit": "^1.5.0",
"eslint-plugin-lit": "^1.5.1",
"flowchart.js": "^1.15.0",
"lit-element": "^2.5.1",
"lit-html": "^1.4.1",

View File

@ -257,6 +257,12 @@ body {
.pf-c-dropdown__toggle::before {
border-color: transparent;
}
.pf-c-toggle-group__button {
color: var(--ak-dark-foreground) !important;
}
.pf-c-toggle-group__button:not(.pf-m-selected) {
background-color: var(--ak-dark-background-light) !important;
}
/* inputs help text */
.pf-c-form__helper-text:not(.pf-m-error) {
color: var(--ak-dark-foreground);

View File

@ -3,7 +3,7 @@ export const SUCCESS_CLASS = "pf-m-success";
export const ERROR_CLASS = "pf-m-danger";
export const PROGRESS_CLASS = "pf-m-in-progress";
export const CURRENT_CLASS = "pf-m-current";
export const VERSION = "2021.6.1-rc1";
export const VERSION = "2021.6.1-rc4";
export const PAGE_SIZE = 20;
export const EVENT_REFRESH = "ak-refresh";
export const EVENT_NOTIFICATION_TOGGLE = "ak-notification-toggle";

View File

@ -1,20 +1,30 @@
import { t } from "@lingui/macro";
import { customElement, html, property, TemplateResult } from "lit-element";
import { CSSResult, customElement, html, property, TemplateResult } from "lit-element";
import { EVENT_REFRESH } from "../../constants";
import { ModalButton } from "../buttons/ModalButton";
import { MessageLevel } from "../messages/Message";
import { showMessage } from "../messages/MessageContainer";
import "../buttons/SpinnerButton";
import { UsedBy, UsedByActionEnum } from "authentik-api";
import PFList from "@patternfly/patternfly/components/List/list.css";
import { until } from "lit-html/directives/until";
@customElement("ak-forms-delete")
export class DeleteForm extends ModalButton {
static get styles(): CSSResult[] {
return super.styles.concat(PFList);
}
@property({attribute: false})
obj?: Record<string, unknown>;
@property()
objectLabel?: string;
@property({attribute: false})
usedBy?: () => Promise<UsedBy[]>;
@property({attribute: false})
delete!: () => Promise<unknown>;
@ -69,6 +79,40 @@ export class DeleteForm extends ModalButton {
</p>
</form>
</section>
${this.usedBy ? until(this.usedBy().then(usedBy => {
if (usedBy.length < 1) {
return html``;
}
return html`
<section class="pf-c-page__main-section">
<form class="pf-c-form pf-m-horizontal">
<p>
${t`The following objects use ${objName} `}
</p>
<ul class="pf-c-list">
${usedBy.map(ub => {
let consequence = "";
switch (ub.action) {
case UsedByActionEnum.Cascade:
consequence = t`object will be DELETED`;
break;
case UsedByActionEnum.CascadeMany:
consequence = t`connecting object will be deleted`;
break;
case UsedByActionEnum.SetDefault:
consequence = t`reference will be reset to default value`;
break;
case UsedByActionEnum.SetNull:
consequence = t`reference will be set to an empty value`;
break;
}
return html`<li>${t`${ub.name} (${consequence})`}</li>`;
})}
</ul>
</form>
</section>
`;
})) : html``}
<footer class="pf-c-modal-box__footer">
<ak-spinner-button
.callAction=${() => {

View File

@ -44,9 +44,14 @@ export class UserOAuthCodeList extends Table<ExpiringBaseGrantModel> {
<ak-forms-delete
.obj=${item}
objectLabel=${t`Authorization Code`}
.usedBy=${() => {
return new Oauth2Api(DEFAULT_CONFIG).oauth2AuthorizationCodesUsedByList({
id: item.pk
});
}}
.delete=${() => {
return new Oauth2Api(DEFAULT_CONFIG).oauth2AuthorizationCodesDestroy({
id: item.pk || 0,
id: item.pk,
});
}}>
<button slot="trigger" class="pf-c-button pf-m-danger">

View File

@ -68,9 +68,14 @@ export class UserOAuthRefreshList extends Table<RefreshTokenModel> {
<ak-forms-delete
.obj=${item}
objectLabel=${t`Refresh Code`}
.usedBy=${() => {
return new Oauth2Api(DEFAULT_CONFIG).oauth2RefreshTokensUsedByList({
id: item.pk
});
}}
.delete=${() => {
return new Oauth2Api(DEFAULT_CONFIG).oauth2RefreshTokensDestroy({
id: item.pk || 0,
id: item.pk,
});
}}>
<button slot="trigger" class="pf-c-button pf-m-danger">

View File

@ -45,6 +45,11 @@ export class AuthenticatedSessionList extends Table<AuthenticatedSession> {
<ak-forms-delete
.obj=${item}
objectLabel=${t`Session`}
.usedBy=${() => {
return new CoreApi(DEFAULT_CONFIG).coreAuthenticatedSessionsUsedByList({
uuid: item.uuid || "",
});
}}
.delete=${() => {
return new CoreApi(DEFAULT_CONFIG).coreAuthenticatedSessionsDestroy({
uuid: item.uuid || "",

View File

@ -40,9 +40,14 @@ export class UserConsentList extends Table<UserConsent> {
<ak-forms-delete
.obj=${item}
objectLabel=${t`Consent`}
.usedBy=${() => {
return new CoreApi(DEFAULT_CONFIG).coreUserConsentUsedByList({
id: item.pk
});
}}
.delete=${() => {
return new CoreApi(DEFAULT_CONFIG).coreUserConsentDestroy({
id: item.pk || 0,
id: item.pk,
});
}}>
<button slot="trigger" class="pf-c-button pf-m-danger">

View File

@ -1144,6 +1144,9 @@ msgid "Disable"
msgstr "Disable"
#: src/pages/user-settings/settings/UserSettingsAuthenticatorDuo.ts
msgid "Disable Duo authenticator"
msgstr "Disable Duo authenticator"
#: src/pages/user-settings/settings/UserSettingsAuthenticatorStatic.ts
msgid "Disable Static Tokens"
msgstr "Disable Static Tokens"
@ -1293,11 +1296,14 @@ msgstr "Email: Text field with Email type."
msgid "Enable"
msgstr "Enable"
#: src/pages/user-settings/settings/UserSettingsAuthenticatorDuo.ts
msgid "Enable Duo authenticator"
msgstr "Enable Duo authenticator"
#: src/pages/sources/ldap/LDAPSourceForm.ts
msgid "Enable StartTLS"
msgstr "Enable StartTLS"
#: src/pages/user-settings/settings/UserSettingsAuthenticatorDuo.ts
#: src/pages/user-settings/settings/UserSettingsAuthenticatorStatic.ts
msgid "Enable Static Tokens"
msgstr "Enable Static Tokens"
@ -2148,6 +2154,10 @@ msgstr "Matches Event's Client IP (strict matching, for network matching use an
msgid "Matches an event against a set of criteria. If any of the configured values match, the policy passes."
msgstr "Matches an event against a set of criteria. If any of the configured values match, the policy passes."
#: src/pages/tenants/TenantForm.ts
msgid "Matching is done based on domain suffix, so if you enter domain.tld, foo.domain.tld will still match."
msgstr "Matching is done based on domain suffix, so if you enter domain.tld, foo.domain.tld will still match."
#: src/pages/policies/expiry/ExpiryPolicyForm.ts
msgid "Maximum age (in days)"
msgstr "Maximum age (in days)"
@ -3805,6 +3815,10 @@ msgstr "The external URL you'll access the application at. Include any non-stand
msgid "The external URL you'll authenticate at. Can be the same domain as authentik."
msgstr "The external URL you'll authenticate at. Can be the same domain as authentik."
#: src/elements/forms/DeleteForm.ts
msgid "The following objects use {objName}"
msgstr "The following objects use {objName}"
#: src/pages/policies/dummy/DummyPolicyForm.ts
msgid "The policy takes a random time to execute. This controls the minimum time it will take."
msgstr "The policy takes a random time to execute. This controls the minimum time it will take."
@ -4519,6 +4533,18 @@ msgstr "authentik LDAP Backend"
msgid "no tabs defined"
msgstr "no tabs defined"
#: src/elements/forms/DeleteForm.ts
msgid "object will be DELETED"
msgstr "object will be DELETED"
#: src/elements/forms/DeleteForm.ts
msgid "reference will be reset to default value"
msgstr "reference will be reset to default value"
#: src/elements/forms/DeleteForm.ts
msgid "reference will be set to an empty value"
msgstr "reference will be set to an empty value"
#: src/elements/Expand.ts
#: src/elements/Expand.ts
msgid "{0}"
@ -4532,6 +4558,10 @@ msgstr "{0} (\"{1}\", of type {2})"
msgid "{0} ({1})"
msgstr "{0} ({1})"
#: src/elements/forms/DeleteForm.ts
msgid "{0} ({consequence})"
msgstr "{0} ({consequence})"
#: src/elements/table/TablePagination.ts
msgid "{0} - {1} of {2}"
msgstr "{0} - {1} of {2}"

View File

@ -1136,6 +1136,9 @@ msgid "Disable"
msgstr ""
#:
msgid "Disable Duo authenticator"
msgstr ""
#:
msgid "Disable Static Tokens"
msgstr ""
@ -1286,10 +1289,13 @@ msgid "Enable"
msgstr ""
#:
msgid "Enable StartTLS"
msgid "Enable Duo authenticator"
msgstr ""
#:
msgid "Enable StartTLS"
msgstr ""
#:
msgid "Enable Static Tokens"
msgstr ""
@ -2140,6 +2146,10 @@ msgstr ""
msgid "Matches an event against a set of criteria. If any of the configured values match, the policy passes."
msgstr ""
#:
msgid "Matching is done based on domain suffix, so if you enter domain.tld, foo.domain.tld will still match."
msgstr ""
#:
msgid "Maximum age (in days)"
msgstr ""
@ -3797,6 +3807,10 @@ msgstr ""
msgid "The external URL you'll authenticate at. Can be the same domain as authentik."
msgstr ""
#:
msgid "The following objects use {objName}"
msgstr ""
#:
msgid "The policy takes a random time to execute. This controls the minimum time it will take."
msgstr ""
@ -4505,6 +4519,18 @@ msgstr ""
msgid "no tabs defined"
msgstr ""
#:
msgid "object will be DELETED"
msgstr ""
#:
msgid "reference will be reset to default value"
msgstr ""
#:
msgid "reference will be set to an empty value"
msgstr ""
#:
#:
msgid "{0}"
@ -4518,6 +4544,10 @@ msgstr ""
msgid "{0} ({1})"
msgstr ""
#:
msgid "{0} ({consequence})"
msgstr ""
#:
msgid "{0} - {1} of {2}"
msgstr ""

View File

@ -103,9 +103,14 @@ export class ApplicationListPage extends TablePage<Application> {
<ak-forms-delete
.obj=${item}
objectLabel=${t`Application`}
.usedBy=${() => {
return new CoreApi(DEFAULT_CONFIG).coreApplicationsUsedByList({
slug: item.slug
});
}}
.delete=${() => {
return new CoreApi(DEFAULT_CONFIG).coreApplicationsDestroy({
slug: item.slug || ""
slug: item.slug
});
}}>
<button slot="trigger" class="pf-c-button pf-m-danger">

View File

@ -79,9 +79,14 @@ export class CertificateKeyPairListPage extends TablePage<CertificateKeyPair> {
<ak-forms-delete
.obj=${item}
objectLabel=${t`Certificate-Key Pair`}
.usedBy=${() => {
return new CryptoApi(DEFAULT_CONFIG).cryptoCertificatekeypairsUsedByList({
kpUuid: item.pk
});
}}
.delete=${() => {
return new CryptoApi(DEFAULT_CONFIG).cryptoCertificatekeypairsDestroy({
kpUuid: item.pk || ""
kpUuid: item.pk
});
}}>
<button slot="trigger" class="pf-c-button pf-m-danger">
@ -119,11 +124,11 @@ export class CertificateKeyPairListPage extends TablePage<CertificateKeyPair> {
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">
<a class="pf-c-button pf-m-secondary" target="_blank"
href="/api/v2beta/crypto/certificatekeypairs/${item.pk}/view_certificate/?download">
href=${item.certificateDownloadUrl}>
${t`Download Certificate`}
</a>
${item.privateKeyAvailable ? html`<a class="pf-c-button pf-m-secondary" target="_blank"
href="/api/v2beta/crypto/certificatekeypairs/${item.pk}/view_private_key/?download">
href=${item.privateKeyDownloadUrl}>
${t`Download Private key`}
</a>` : html``}
</div>

View File

@ -1,7 +1,7 @@
import { t } from "@lingui/macro";
import { css, CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
import { until } from "lit-html/directives/until";
import { ActionEnum, FlowsApi } from "authentik-api";
import { EventMatcherPolicyActionEnum, FlowsApi } from "authentik-api";
import "../../elements/Spinner";
import "../../elements/Expand";
import { PFSize } from "../../elements/Spinner";
@ -142,14 +142,14 @@ export class EventInfo extends LitElement {
return html`<ak-spinner size=${PFSize.Medium}></ak-spinner>`;
}
switch (this.event?.action) {
case ActionEnum.ModelCreated:
case ActionEnum.ModelUpdated:
case ActionEnum.ModelDeleted:
case EventMatcherPolicyActionEnum.ModelCreated:
case EventMatcherPolicyActionEnum.ModelUpdated:
case EventMatcherPolicyActionEnum.ModelDeleted:
return html`
<h3>${t`Affected model:`}</h3>
${this.getModelInfo(this.event.context?.model as EventContext)}
`;
case ActionEnum.AuthorizeApplication:
case EventMatcherPolicyActionEnum.AuthorizeApplication:
return html`<div class="pf-l-flex">
<div class="pf-l-flex__item">
<h3>${t`Authorized application:`}</h3>
@ -166,17 +166,17 @@ export class EventInfo extends LitElement {
</div>
</div>
<ak-expand>${this.defaultResponse()}</ak-expand>`;
case ActionEnum.EmailSent:
case EventMatcherPolicyActionEnum.EmailSent:
return html`<h3>${t`Email info:`}</h3>
${this.getEmailInfo(this.event.context)}
<ak-expand>
<iframe srcdoc=${this.event.context.body}></iframe>
</ak-expand>`;
case ActionEnum.SecretView:
case EventMatcherPolicyActionEnum.SecretView:
return html`
<h3>${t`Secret:`}</h3>
${this.getModelInfo(this.event.context.secret as EventContext)}`;
case ActionEnum.PropertyMappingException:
case EventMatcherPolicyActionEnum.PropertyMappingException:
return html`<div class="pf-l-flex">
<div class="pf-l-flex__item">
<h3>${t`Exception`}</h3>
@ -188,7 +188,7 @@ export class EventInfo extends LitElement {
</div>
</div>
<ak-expand>${this.defaultResponse()}</ak-expand>`;
case ActionEnum.PolicyException:
case EventMatcherPolicyActionEnum.PolicyException:
return html`<div class="pf-l-flex">
<div class="pf-l-flex__item">
<h3>${t`Binding`}</h3>
@ -207,7 +207,7 @@ export class EventInfo extends LitElement {
</div>
</div>
<ak-expand>${this.defaultResponse()}</ak-expand>`;
case ActionEnum.PolicyExecution:
case EventMatcherPolicyActionEnum.PolicyExecution:
return html`<div class="pf-l-flex">
<div class="pf-l-flex__item">
<h3>${t`Binding`}</h3>
@ -235,10 +235,10 @@ export class EventInfo extends LitElement {
</div>
</div>
<ak-expand>${this.defaultResponse()}</ak-expand>`;
case ActionEnum.ConfigurationError:
case EventMatcherPolicyActionEnum.ConfigurationError:
return html`<h3>${this.event.context.message}</h3>
<ak-expand>${this.defaultResponse()}</ak-expand>`;
case ActionEnum.UpdateAvailable:
case EventMatcherPolicyActionEnum.UpdateAvailable:
return html`<h3>${t`New version available!`}</h3>
<a
target="_blank"
@ -247,7 +247,7 @@ export class EventInfo extends LitElement {
</a>`;
// Action types which typically don't record any extra context.
// If context is not empty, we fall to the default response.
case ActionEnum.Login:
case EventMatcherPolicyActionEnum.Login:
if ("using_source" in this.event.context) {
return html`<div class="pf-l-flex">
<div class="pf-l-flex__item">
@ -257,11 +257,11 @@ export class EventInfo extends LitElement {
</div>`;
}
return this.defaultResponse();
case ActionEnum.LoginFailed:
case EventMatcherPolicyActionEnum.LoginFailed:
return html`
<h3>${t`Attempted to log in as ${this.event.context.username}`}</h3>
<ak-expand>${this.defaultResponse()}</ak-expand>`;
case ActionEnum.Logout:
case EventMatcherPolicyActionEnum.Logout:
if (this.event.context === {}) {
return html`<span>${t`No additional data available.`}</span>`;
}

View File

@ -73,9 +73,14 @@ export class RuleListPage extends TablePage<NotificationRule> {
<ak-forms-delete
.obj=${item}
objectLabel=${t`Notification rule`}
.usedBy=${() => {
return new EventsApi(DEFAULT_CONFIG).eventsRulesUsedByList({
pbmUuid: item.pk
});
}}
.delete=${() => {
return new EventsApi(DEFAULT_CONFIG).eventsRulesDestroy({
pbmUuid: item.pk || ""
pbmUuid: item.pk
});
}}>
<button slot="trigger" class="pf-c-button pf-m-danger">

View File

@ -77,9 +77,14 @@ export class TransportListPage extends TablePage<NotificationTransport> {
<ak-forms-delete
.obj=${item}
objectLabel=${t`Notifications Transport`}
.usedBy=${() => {
return new EventsApi(DEFAULT_CONFIG).eventsTransportsUsedByList({
uuid: item.pk
});
}}
.delete=${() => {
return new EventsApi(DEFAULT_CONFIG).eventsTransportsDestroy({
uuid: item.pk || ""
uuid: item.pk
});
}}>
<button slot="trigger" class="pf-c-button pf-m-danger">

View File

@ -82,9 +82,14 @@ export class BoundStagesList extends Table<FlowStageBinding> {
<ak-forms-delete
.obj=${item}
objectLabel=${t`Stage binding`}
.usedBy=${() => {
return new FlowsApi(DEFAULT_CONFIG).flowsBindingsUsedByList({
fsbUuid: item.pk
});
}}
.delete=${() => {
return new FlowsApi(DEFAULT_CONFIG).flowsBindingsDestroy({
fsbUuid: item.pk || "",
fsbUuid: item.pk,
});
}}>
<button slot="trigger" class="pf-c-button pf-m-danger">

View File

@ -78,6 +78,11 @@ export class FlowListPage extends TablePage<Flow> {
<ak-forms-delete
.obj=${item}
objectLabel=${t`Flow`}
.usedBy=${() => {
return new FlowsApi(DEFAULT_CONFIG).flowsInstancesUsedByList({
slug: item.slug
});
}}
.delete=${() => {
return new FlowsApi(DEFAULT_CONFIG).flowsInstancesDestroy({
slug: item.slug
@ -98,7 +103,7 @@ export class FlowListPage extends TablePage<Flow> {
}}>
${t`Execute`}
</button>
<a class="pf-c-button pf-m-secondary" href="/api/v2beta/flows/instances/${item.slug}/export/">
<a class="pf-c-button pf-m-secondary" href=${flow.exportUrl}>
${t`Export`}
</a>`,
];

View File

@ -72,9 +72,14 @@ export class GroupListPage extends TablePage<Group> {
<ak-forms-delete
.obj=${item}
objectLabel=${t`Group`}
.usedBy=${() => {
return new CoreApi(DEFAULT_CONFIG).coreGroupsUsedByList({
groupUuid: item.pk
});
}}
.delete=${() => {
return new CoreApi(DEFAULT_CONFIG).coreGroupsDestroy({
groupUuid: item.pk || ""
groupUuid: item.pk
});
}}>
<button slot="trigger" class="pf-c-button pf-m-danger">

View File

@ -77,9 +77,14 @@ export class OutpostListPage extends TablePage<Outpost> {
<ak-forms-delete
.obj=${item}
objectLabel=${t`Outpost`}
.usedBy=${() => {
return new OutpostsApi(DEFAULT_CONFIG).outpostsInstancesUsedByList({
uuid: item.pk
});
}}
.delete=${() => {
return new OutpostsApi(DEFAULT_CONFIG).outpostsInstancesDestroy({
uuid: item.pk || ""
uuid: item.pk
});
}}>
<button slot="trigger" class="pf-c-button pf-m-danger">

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