From d811aabd38a73a6cd626befddbb8dbdb75b63d71 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Tue, 1 Aug 2023 19:28:38 +0200 Subject: [PATCH] blueprints: initial Signed-off-by: Jens Langhammer --- authentik/api/apps.py | 2 +- authentik/blueprints/api.py | 60 +++++++++++++++++++++++++++++++++-- schema.yml | 62 +++++++++++++++++++++++++++++++++++-- 3 files changed, 119 insertions(+), 5 deletions(-) diff --git a/authentik/api/apps.py b/authentik/api/apps.py index 7b91d99a9..c6720f1b9 100644 --- a/authentik/api/apps.py +++ b/authentik/api/apps.py @@ -31,5 +31,5 @@ class AuthentikAPIConfig(AppConfig): "type": "apiKey", "in": "header", "name": "Authorization", - "scheme": "bearer", + "scheme": "Bearer", } diff --git a/authentik/blueprints/api.py b/authentik/blueprints/api.py index d13a30812..d3bed1a1c 100644 --- a/authentik/blueprints/api.py +++ b/authentik/blueprints/api.py @@ -1,9 +1,10 @@ """Serializer mixin for managed models""" +from django.apps import apps from django.utils.translation import gettext_lazy as _ from drf_spectacular.utils import extend_schema, inline_serializer from rest_framework.decorators import action from rest_framework.exceptions import ValidationError -from rest_framework.fields import CharField, DateTimeField, JSONField +from rest_framework.fields import CharField, DateTimeField, DictField, JSONField from rest_framework.permissions import IsAdminUser from rest_framework.request import Request from rest_framework.response import Response @@ -12,7 +13,8 @@ from rest_framework.viewsets import ModelViewSet from authentik.api.decorators import permission_required from authentik.blueprints.models import BlueprintInstance -from authentik.blueprints.v1.importer import StringImporter +from authentik.blueprints.v1.common import Blueprint, BlueprintEntry, BlueprintEntryDesiredState +from authentik.blueprints.v1.importer import StringImporter, is_model_allowed from authentik.blueprints.v1.oci import OCI_PREFIX from authentik.blueprints.v1.tasks import apply_blueprint, blueprints_find_dict from authentik.core.api.used_by import UsedByMixin @@ -84,6 +86,32 @@ class BlueprintInstanceSerializer(ModelSerializer): } +class BlueprintEntrySerializer(PassiveSerializer): + """Validate a single blueprint entry, similar to a subset of regular blueprints""" + + model = CharField() + attrs = DictField() + + def validate_model(self, fq_model: str) -> str: + """Validate model is allowed""" + if "." not in fq_model: + raise ValidationError("Invalid model") + app, model_name = fq_model.split(".") + try: + model = apps.get_model(app, model_name) + if not is_model_allowed(model): + raise ValidationError("Invalid model") + except LookupError: + raise ValidationError("Invalid model") + return model + + +class BlueprintProceduralSerializer(PassiveSerializer): + """Validate a procedural blueprint, which is a subset of a regular blueprint""" + + entries = ListSerializer(child=BlueprintEntrySerializer()) + + class BlueprintInstanceViewSet(UsedByMixin, ModelViewSet): """Blueprint instances""" @@ -127,3 +155,31 @@ class BlueprintInstanceViewSet(UsedByMixin, ModelViewSet): blueprint = self.get_object() apply_blueprint.delay(str(blueprint.pk)).get() return self.retrieve(request, *args, **kwargs) + + @extend_schema( + request=BlueprintProceduralSerializer, + ) + @action( + detail=False, + pagination_class=None, + filter_backends=[], + methods=["PUT"], + permission_classes=[IsAdminUser], + ) + def procedural(self, request: Request) -> Response: + blueprint = Blueprint() + data = BlueprintProceduralSerializer(data=request.data) + data.is_valid(raise_exception=True) + for raw_entry in data.validated_data["entries"]: + entry = BlueprintEntrySerializer(data=raw_entry) + entry.is_valid(raise_exception=True) + blueprint.entries.append( + BlueprintEntry( + model=entry.data["model"], + state=BlueprintEntryDesiredState.PRESENT, + identifiers={}, + attrs=entry.data["attrs"], + ) + ) + print(blueprint) + return Response(status=400) diff --git a/schema.yml b/schema.yml index 299729ab1..412aeb02b 100644 --- a/schema.yml +++ b/schema.yml @@ -8513,6 +8513,39 @@ paths: schema: $ref: '#/components/schemas/GenericError' description: '' + /managed/blueprints/procedural/: + put: + operationId: managed_blueprints_procedural_update + description: Blueprint instances + tags: + - managed + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/BlueprintProceduralRequest' + required: true + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/BlueprintInstance' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' /oauth2/access_tokens/: get: operationId: oauth2_access_tokens_list @@ -28000,6 +28033,20 @@ components: * `REDIRECT` - Redirect Binding * `POST` - POST Binding * `POST_AUTO` - POST Binding with auto-confirmation + BlueprintEntryRequest: + type: object + description: Validate a single blueprint entry, similar to a subset of regular + blueprints + properties: + model: + type: string + minLength: 1 + attrs: + type: object + additionalProperties: {} + required: + - attrs + - model BlueprintFile: type: object properties: @@ -28101,6 +28148,17 @@ components: * `error` - Error * `orphaned` - Orphaned * `unknown` - Unknown + BlueprintProceduralRequest: + type: object + description: Validate a procedural blueprint, which is a subset of a regular + blueprint + properties: + entries: + type: array + items: + $ref: '#/components/schemas/BlueprintEntryRequest' + required: + - entries Cache: type: object description: Generic cache stats for an object @@ -40941,6 +40999,6 @@ components: type: apiKey in: header name: Authorization - scheme: bearer + scheme: Bearer servers: -- url: /api/v3/ +- url: /api/v3