From a331affd42aa8216e50c1f5f08b3f866aca9023b Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Mon, 7 Aug 2023 21:02:02 +0200 Subject: [PATCH] actually make API work Signed-off-by: Jens Langhammer --- authentik/blueprints/api.py | 56 +++++++++++++++++++++----- authentik/blueprints/v1/importer.py | 1 + authentik/blueprints/v1/json_parser.py | 13 ++++-- schema.yml | 40 ++++++++++++++++-- 4 files changed, 92 insertions(+), 18 deletions(-) diff --git a/authentik/blueprints/api.py b/authentik/blueprints/api.py index c38ccbe40..3d768197c 100644 --- a/authentik/blueprints/api.py +++ b/authentik/blueprints/api.py @@ -1,10 +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 drf_spectacular.utils import OpenApiParameter, extend_schema, inline_serializer from rest_framework.decorators import action from rest_framework.exceptions import ValidationError -from rest_framework.fields import CharField, DateTimeField, DictField, JSONField +from rest_framework.fields import BooleanField, CharField, DateTimeField, DictField, JSONField from rest_framework.permissions import IsAdminUser from rest_framework.request import Request from rest_framework.response import Response @@ -14,7 +14,7 @@ from rest_framework.viewsets import ModelViewSet from authentik.api.decorators import permission_required from authentik.blueprints.models import BlueprintInstance from authentik.blueprints.v1.common import Blueprint, BlueprintEntry, BlueprintEntryDesiredState -from authentik.blueprints.v1.importer import YAMLStringImporter, is_model_allowed +from authentik.blueprints.v1.importer import Importer, YAMLStringImporter, is_model_allowed from authentik.blueprints.v1.json_parser import BlueprintJSONParser from authentik.blueprints.v1.oci import OCI_PREFIX from authentik.blueprints.v1.tasks import apply_blueprint, blueprints_find_dict @@ -91,6 +91,8 @@ class BlueprintEntrySerializer(PassiveSerializer): """Validate a single blueprint entry, similar to a subset of regular blueprints""" model = CharField() + id = CharField(required=False, allow_blank=True) + identifiers = DictField() attrs = DictField() def validate_model(self, fq_model: str) -> str: @@ -104,13 +106,22 @@ class BlueprintEntrySerializer(PassiveSerializer): raise ValidationError("Invalid model") except LookupError: raise ValidationError("Invalid model") - return model + return fq_model -class BlueprintProceduralSerializer(PassiveSerializer): +class BlueprintSerializer(PassiveSerializer): """Validate a procedural blueprint, which is a subset of a regular blueprint""" entries = ListSerializer(child=BlueprintEntrySerializer()) + context = DictField(required=False) + + +class BlueprintProceduralResultSerializer(PassiveSerializer): + """Result of applying a procedural blueprint""" + + valid = BooleanField() + applied = BooleanField() + logs = ListSerializer(child=CharField()) class BlueprintInstanceViewSet(UsedByMixin, ModelViewSet): @@ -158,7 +169,11 @@ class BlueprintInstanceViewSet(UsedByMixin, ModelViewSet): return self.retrieve(request, *args, **kwargs) @extend_schema( - request=BlueprintProceduralSerializer, + request=BlueprintSerializer, + responses=BlueprintProceduralResultSerializer, + parameters=[ + OpenApiParameter("validate_only", bool), + ], ) @action( detail=False, @@ -169,19 +184,38 @@ class BlueprintInstanceViewSet(UsedByMixin, ModelViewSet): parser_classes=[BlueprintJSONParser], ) def procedural(self, request: Request) -> Response: + """Run a client-provided blueprint once, as-is. Blueprint is not kept in memory/database + and will not be continuously applied""" blueprint = Blueprint() - data = BlueprintProceduralSerializer(data=request.data) + data = BlueprintSerializer(data=request.data) data.is_valid(raise_exception=True) + blueprint.context = data.validated_data.get("context", {}) 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={}, + state=BlueprintEntryDesiredState.MUST_CREATED, + identifiers=entry.data["identifiers"], attrs=entry.data["attrs"], + id=entry.data.get("id", None), ) ) - print(blueprint) - return Response(status=400) + importer = Importer(blueprint) + valid, logs = importer.validate() + result = { + "valid": valid, + "applied": False, + # TODO: Better way to handle logs + "logs": [x["event"] for x in logs], + } + response = BlueprintProceduralResultSerializer(data=result) + response.is_valid() + if request.query_params.get("validate_only", False): + return Response(response.validated_data) + applied = importer.apply() + result["applied"] = applied + response = BlueprintProceduralResultSerializer(data=result) + response.is_valid() + return Response(response.validated_data) diff --git a/authentik/blueprints/v1/importer.py b/authentik/blueprints/v1/importer.py index 7b043c749..d5fb626f5 100644 --- a/authentik/blueprints/v1/importer.py +++ b/authentik/blueprints/v1/importer.py @@ -242,6 +242,7 @@ class Importer: def apply(self) -> bool: """Apply (create/update) models yaml, in database transaction""" + self.logger.debug("Starting blueprint import") try: with transaction.atomic(): if not self._apply_models(): diff --git a/authentik/blueprints/v1/json_parser.py b/authentik/blueprints/v1/json_parser.py index 1ca670e65..8a77800c2 100644 --- a/authentik/blueprints/v1/json_parser.py +++ b/authentik/blueprints/v1/json_parser.py @@ -1,8 +1,12 @@ """Blueprint JSON decoder""" +import codecs from collections.abc import Hashable from typing import Any +from django.conf import settings +from rest_framework.exceptions import ParseError from rest_framework.parsers import JSONParser +from yaml import load from yaml.nodes import MappingNode from authentik.blueprints.v1.common import BlueprintLoader, YAMLTag, yaml_key_map @@ -65,6 +69,9 @@ class BlueprintJSONParser(JSONParser): """Wrapper around the rest_framework JSON parser that uses the `BlueprintJSONDecoder`""" def parse(self, stream, media_type=None, parser_context=None): - parser_context = parser_context or {} - parser_context["cls"] = BlueprintJSONDecoder - return super().parse(stream, media_type, parser_context) + encoding = parser_context.get("encoding", settings.DEFAULT_CHARSET) + try: + decoded_stream = codecs.getreader(encoding)(stream) + return load(decoded_stream, BlueprintJSONDecoder) + except ValueError as exc: + raise ParseError("JSON parse error") from exc diff --git a/schema.yml b/schema.yml index b1c26397e..f2173bc18 100644 --- a/schema.yml +++ b/schema.yml @@ -8483,14 +8483,21 @@ paths: /managed/blueprints/procedural/: put: operationId: managed_blueprints_procedural_update - description: Blueprint instances + description: |- + Run a client-provided blueprint once, as-is. Blueprint is not kept in memory/database + and will not be continuously applied + parameters: + - in: query + name: validate_only + schema: + type: boolean tags: - managed requestBody: content: application/json: schema: - $ref: '#/components/schemas/BlueprintProceduralRequest' + $ref: '#/components/schemas/BlueprintRequest' required: true security: - authentik: [] @@ -8499,7 +8506,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/BlueprintInstance' + $ref: '#/components/schemas/BlueprintProceduralResult' description: '' '400': content: @@ -28008,11 +28015,17 @@ components: model: type: string minLength: 1 + id: + type: string + identifiers: + type: object + additionalProperties: {} attrs: type: object additionalProperties: {} required: - attrs + - identifiers - model BlueprintFile: type: object @@ -28115,7 +28128,23 @@ components: * `error` - Error * `orphaned` - Orphaned * `unknown` - Unknown - BlueprintProceduralRequest: + BlueprintProceduralResult: + type: object + description: Result of applying a procedural blueprint + properties: + valid: + type: boolean + applied: + type: boolean + logs: + type: array + items: + type: string + required: + - applied + - logs + - valid + BlueprintRequest: type: object description: Validate a procedural blueprint, which is a subset of a regular blueprint @@ -28124,6 +28153,9 @@ components: type: array items: $ref: '#/components/schemas/BlueprintEntryRequest' + context: + type: object + additionalProperties: {} required: - entries Cache: