actually make API work
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
parent
9d3bd8418d
commit
a331affd42
|
@ -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)
|
||||||
|
|
|
@ -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():
|
||||||
|
|
|
@ -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
|
||||||
|
|
40
schema.yml
40
schema.yml
|
@ -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:
|
||||||
|
|
Reference in a new issue