actually make API work

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
Jens Langhammer 2023-08-07 21:02:02 +02:00
parent 9d3bd8418d
commit a331affd42
No known key found for this signature in database
4 changed files with 92 additions and 18 deletions

View file

@ -1,10 +1,10 @@
"""Serializer mixin for managed models""" """Serializer mixin for managed models"""
from django.apps import apps from django.apps import apps
from django.utils.translation import gettext_lazy as _ 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.decorators import action
from rest_framework.exceptions import ValidationError 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.permissions import IsAdminUser
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response 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.api.decorators import permission_required
from authentik.blueprints.models import BlueprintInstance from authentik.blueprints.models import BlueprintInstance
from authentik.blueprints.v1.common import Blueprint, BlueprintEntry, BlueprintEntryDesiredState 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.json_parser import BlueprintJSONParser
from authentik.blueprints.v1.oci import OCI_PREFIX from authentik.blueprints.v1.oci import OCI_PREFIX
from authentik.blueprints.v1.tasks import apply_blueprint, blueprints_find_dict 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""" """Validate a single blueprint entry, similar to a subset of regular blueprints"""
model = CharField() model = CharField()
id = CharField(required=False, allow_blank=True)
identifiers = DictField()
attrs = DictField() attrs = DictField()
def validate_model(self, fq_model: str) -> str: def validate_model(self, fq_model: str) -> str:
@ -104,13 +106,22 @@ class BlueprintEntrySerializer(PassiveSerializer):
raise ValidationError("Invalid model") raise ValidationError("Invalid model")
except LookupError: except LookupError:
raise ValidationError("Invalid model") 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""" """Validate a procedural blueprint, which is a subset of a regular blueprint"""
entries = ListSerializer(child=BlueprintEntrySerializer()) 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): class BlueprintInstanceViewSet(UsedByMixin, ModelViewSet):
@ -158,7 +169,11 @@ class BlueprintInstanceViewSet(UsedByMixin, ModelViewSet):
return self.retrieve(request, *args, **kwargs) return self.retrieve(request, *args, **kwargs)
@extend_schema( @extend_schema(
request=BlueprintProceduralSerializer, request=BlueprintSerializer,
responses=BlueprintProceduralResultSerializer,
parameters=[
OpenApiParameter("validate_only", bool),
],
) )
@action( @action(
detail=False, detail=False,
@ -169,19 +184,38 @@ class BlueprintInstanceViewSet(UsedByMixin, ModelViewSet):
parser_classes=[BlueprintJSONParser], parser_classes=[BlueprintJSONParser],
) )
def procedural(self, request: Request) -> Response: 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() blueprint = Blueprint()
data = BlueprintProceduralSerializer(data=request.data) data = BlueprintSerializer(data=request.data)
data.is_valid(raise_exception=True) data.is_valid(raise_exception=True)
blueprint.context = data.validated_data.get("context", {})
for raw_entry in data.validated_data["entries"]: for raw_entry in data.validated_data["entries"]:
entry = BlueprintEntrySerializer(data=raw_entry) entry = BlueprintEntrySerializer(data=raw_entry)
entry.is_valid(raise_exception=True) entry.is_valid(raise_exception=True)
blueprint.entries.append( blueprint.entries.append(
BlueprintEntry( BlueprintEntry(
model=entry.data["model"], model=entry.data["model"],
state=BlueprintEntryDesiredState.PRESENT, state=BlueprintEntryDesiredState.MUST_CREATED,
identifiers={}, identifiers=entry.data["identifiers"],
attrs=entry.data["attrs"], attrs=entry.data["attrs"],
id=entry.data.get("id", None),
) )
) )
print(blueprint) importer = Importer(blueprint)
return Response(status=400) 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)

View file

@ -242,6 +242,7 @@ class Importer:
def apply(self) -> bool: def apply(self) -> bool:
"""Apply (create/update) models yaml, in database transaction""" """Apply (create/update) models yaml, in database transaction"""
self.logger.debug("Starting blueprint import")
try: try:
with transaction.atomic(): with transaction.atomic():
if not self._apply_models(): if not self._apply_models():

View file

@ -1,8 +1,12 @@
"""Blueprint JSON decoder""" """Blueprint JSON decoder"""
import codecs
from collections.abc import Hashable from collections.abc import Hashable
from typing import Any from typing import Any
from django.conf import settings
from rest_framework.exceptions import ParseError
from rest_framework.parsers import JSONParser from rest_framework.parsers import JSONParser
from yaml import load
from yaml.nodes import MappingNode from yaml.nodes import MappingNode
from authentik.blueprints.v1.common import BlueprintLoader, YAMLTag, yaml_key_map 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`""" """Wrapper around the rest_framework JSON parser that uses the `BlueprintJSONDecoder`"""
def parse(self, stream, media_type=None, parser_context=None): def parse(self, stream, media_type=None, parser_context=None):
parser_context = parser_context or {} encoding = parser_context.get("encoding", settings.DEFAULT_CHARSET)
parser_context["cls"] = BlueprintJSONDecoder try:
return super().parse(stream, media_type, parser_context) decoded_stream = codecs.getreader(encoding)(stream)
return load(decoded_stream, BlueprintJSONDecoder)
except ValueError as exc:
raise ParseError("JSON parse error") from exc

View file

@ -8483,14 +8483,21 @@ paths:
/managed/blueprints/procedural/: /managed/blueprints/procedural/:
put: put:
operationId: managed_blueprints_procedural_update 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: tags:
- managed - managed
requestBody: requestBody:
content: content:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/BlueprintProceduralRequest' $ref: '#/components/schemas/BlueprintRequest'
required: true required: true
security: security:
- authentik: [] - authentik: []
@ -8499,7 +8506,7 @@ paths:
content: content:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/BlueprintInstance' $ref: '#/components/schemas/BlueprintProceduralResult'
description: '' description: ''
'400': '400':
content: content:
@ -28008,11 +28015,17 @@ components:
model: model:
type: string type: string
minLength: 1 minLength: 1
id:
type: string
identifiers:
type: object
additionalProperties: {}
attrs: attrs:
type: object type: object
additionalProperties: {} additionalProperties: {}
required: required:
- attrs - attrs
- identifiers
- model - model
BlueprintFile: BlueprintFile:
type: object type: object
@ -28115,7 +28128,23 @@ components:
* `error` - Error * `error` - Error
* `orphaned` - Orphaned * `orphaned` - Orphaned
* `unknown` - Unknown * `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 type: object
description: Validate a procedural blueprint, which is a subset of a regular description: Validate a procedural blueprint, which is a subset of a regular
blueprint blueprint
@ -28124,6 +28153,9 @@ components:
type: array type: array
items: items:
$ref: '#/components/schemas/BlueprintEntryRequest' $ref: '#/components/schemas/BlueprintEntryRequest'
context:
type: object
additionalProperties: {}
required: required:
- entries - entries
Cache: Cache: