cleanup
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
parent
edc7f2fdb0
commit
629af26742
|
@ -1,97 +0,0 @@
|
||||||
from django.apps import apps
|
|
||||||
from drf_spectacular.utils import PolymorphicProxySerializer, extend_schema, extend_schema_field
|
|
||||||
from rest_framework.exceptions import ValidationError
|
|
||||||
from rest_framework.fields import ChoiceField, DictField
|
|
||||||
from rest_framework.permissions import IsAdminUser
|
|
||||||
from rest_framework.request import Request
|
|
||||||
from rest_framework.response import Response
|
|
||||||
from rest_framework.views import APIView
|
|
||||||
from yaml import ScalarNode
|
|
||||||
from authentik.blueprints.v1.common import Blueprint, BlueprintEntry, BlueprintEntryDesiredState, KeyOf
|
|
||||||
from authentik.blueprints.v1.importer import Importer
|
|
||||||
|
|
||||||
from authentik.core.api.applications import ApplicationSerializer
|
|
||||||
from authentik.core.api.utils import PassiveSerializer
|
|
||||||
from authentik.core.models import Provider
|
|
||||||
from authentik.lib.utils.reflection import all_subclasses
|
|
||||||
|
|
||||||
|
|
||||||
def get_provider_serializer_mapping():
|
|
||||||
map = {}
|
|
||||||
for model in all_subclasses(Provider):
|
|
||||||
if model._meta.abstract:
|
|
||||||
continue
|
|
||||||
map[f"{model._meta.app_label}.{model._meta.model_name}"] = model().serializer
|
|
||||||
return map
|
|
||||||
|
|
||||||
|
|
||||||
@extend_schema_field(
|
|
||||||
PolymorphicProxySerializer(
|
|
||||||
component_name="model",
|
|
||||||
serializers=get_provider_serializer_mapping,
|
|
||||||
resource_type_field_name="provider_model",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
class TransactionProviderField(DictField):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class TransactionApplicationSerializer(PassiveSerializer):
|
|
||||||
"""Serializer for creating a provider and an application in one transaction"""
|
|
||||||
|
|
||||||
app = ApplicationSerializer()
|
|
||||||
provider_model = ChoiceField(choices=list(get_provider_serializer_mapping().keys()))
|
|
||||||
provider = TransactionProviderField()
|
|
||||||
|
|
||||||
_provider_model: type[Provider] = None
|
|
||||||
|
|
||||||
def validate_provider_model(self, fq_model_name: str) -> str:
|
|
||||||
"""Validate that the model exists and is a provider"""
|
|
||||||
if "." not in fq_model_name:
|
|
||||||
raise ValidationError("Invalid provider model")
|
|
||||||
try:
|
|
||||||
app, model_name = fq_model_name.split(".")
|
|
||||||
model = apps.get_model(app, model_name)
|
|
||||||
if not issubclass(model, Provider):
|
|
||||||
raise ValidationError("Invalid provider model")
|
|
||||||
self._provider_model = model
|
|
||||||
except LookupError:
|
|
||||||
raise ValidationError("Invalid provider model")
|
|
||||||
return fq_model_name
|
|
||||||
|
|
||||||
def validate_provider(self, provider: dict) -> dict:
|
|
||||||
"""Validate provider data"""
|
|
||||||
# ensure the model has been validated
|
|
||||||
self.validate_provider_model(self.initial_data["provider_model"])
|
|
||||||
model_serializer = self._provider_model().serializer(data=provider)
|
|
||||||
model_serializer.is_valid(raise_exception=True)
|
|
||||||
return model_serializer.validated_data
|
|
||||||
|
|
||||||
|
|
||||||
class TransactionalApplicationView(APIView):
|
|
||||||
permission_classes = [IsAdminUser]
|
|
||||||
|
|
||||||
@extend_schema(request=TransactionApplicationSerializer())
|
|
||||||
def put(self, request: Request) -> Response:
|
|
||||||
data = TransactionApplicationSerializer(data=request.data)
|
|
||||||
data.is_valid(raise_exception=True)
|
|
||||||
print(data.validated_data)
|
|
||||||
|
|
||||||
blueprint = Blueprint()
|
|
||||||
blueprint.entries.append(BlueprintEntry(
|
|
||||||
model=data.validated_data["provider_model"],
|
|
||||||
state=BlueprintEntryDesiredState.PRESENT,
|
|
||||||
identifiers={},
|
|
||||||
id="provider",
|
|
||||||
attrs=data.validated_data["provider"],
|
|
||||||
))
|
|
||||||
app_data = data.validated_data["app"]
|
|
||||||
app_data["provider"] = KeyOf(None, ScalarNode(value="provider"))
|
|
||||||
blueprint.entries.append(BlueprintEntry(
|
|
||||||
model="authentik_core.application",
|
|
||||||
state=BlueprintEntryDesiredState.PRESENT,
|
|
||||||
identifiers={},
|
|
||||||
attrs=app_data,
|
|
||||||
))
|
|
||||||
importer = Importer("", {})
|
|
||||||
return Response(status=200)
|
|
|
@ -0,0 +1,127 @@
|
||||||
|
"""transactional application and provider creation"""
|
||||||
|
from django.apps import apps
|
||||||
|
from drf_spectacular.utils import PolymorphicProxySerializer, extend_schema, extend_schema_field
|
||||||
|
from rest_framework.exceptions import ValidationError
|
||||||
|
from rest_framework.fields import BooleanField, CharField, ChoiceField, DictField, ListField
|
||||||
|
from rest_framework.permissions import IsAdminUser
|
||||||
|
from rest_framework.request import Request
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
from yaml import ScalarNode
|
||||||
|
|
||||||
|
from authentik.blueprints.v1.common import (
|
||||||
|
Blueprint,
|
||||||
|
BlueprintEntry,
|
||||||
|
BlueprintEntryDesiredState,
|
||||||
|
KeyOf,
|
||||||
|
)
|
||||||
|
from authentik.blueprints.v1.importer import Importer
|
||||||
|
from authentik.core.api.applications import ApplicationSerializer
|
||||||
|
from authentik.core.api.utils import PassiveSerializer
|
||||||
|
from authentik.core.models import Provider
|
||||||
|
from authentik.lib.utils.reflection import all_subclasses
|
||||||
|
|
||||||
|
|
||||||
|
def get_provider_serializer_mapping():
|
||||||
|
"""Get a mapping of all providers' model names and their serializers"""
|
||||||
|
mapping = {}
|
||||||
|
for model in all_subclasses(Provider):
|
||||||
|
if model._meta.abstract:
|
||||||
|
continue
|
||||||
|
mapping[f"{model._meta.app_label}.{model._meta.model_name}"] = model().serializer
|
||||||
|
return mapping
|
||||||
|
|
||||||
|
|
||||||
|
@extend_schema_field(
|
||||||
|
PolymorphicProxySerializer(
|
||||||
|
component_name="model",
|
||||||
|
serializers=get_provider_serializer_mapping,
|
||||||
|
resource_type_field_name="provider_model",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
class TransactionProviderField(DictField):
|
||||||
|
"""Dictionary field which can hold provider creation data"""
|
||||||
|
|
||||||
|
|
||||||
|
class TransactionApplicationSerializer(PassiveSerializer):
|
||||||
|
"""Serializer for creating a provider and an application in one transaction"""
|
||||||
|
|
||||||
|
app = ApplicationSerializer()
|
||||||
|
provider_model = ChoiceField(choices=list(get_provider_serializer_mapping().keys()))
|
||||||
|
provider = TransactionProviderField()
|
||||||
|
|
||||||
|
_provider_model: type[Provider] = None
|
||||||
|
|
||||||
|
def validate_provider_model(self, fq_model_name: str) -> str:
|
||||||
|
"""Validate that the model exists and is a provider"""
|
||||||
|
if "." not in fq_model_name:
|
||||||
|
raise ValidationError("Invalid provider model")
|
||||||
|
try:
|
||||||
|
app, _, model_name = fq_model_name.partition(".")
|
||||||
|
model = apps.get_model(app, model_name)
|
||||||
|
if not issubclass(model, Provider):
|
||||||
|
raise ValidationError("Invalid provider model")
|
||||||
|
self._provider_model = model
|
||||||
|
except LookupError:
|
||||||
|
raise ValidationError("Invalid provider model")
|
||||||
|
return fq_model_name
|
||||||
|
|
||||||
|
|
||||||
|
class TransactionApplicationResponseSerializer(PassiveSerializer):
|
||||||
|
"""Transactional creation response"""
|
||||||
|
|
||||||
|
valid = BooleanField()
|
||||||
|
applied = BooleanField()
|
||||||
|
logs = ListField(child=CharField())
|
||||||
|
|
||||||
|
|
||||||
|
class TransactionalApplicationView(APIView):
|
||||||
|
"""Create provider and application and attach them in a single transaction"""
|
||||||
|
|
||||||
|
permission_classes = [IsAdminUser]
|
||||||
|
|
||||||
|
@extend_schema(
|
||||||
|
request=TransactionApplicationSerializer(),
|
||||||
|
responses={
|
||||||
|
200: TransactionApplicationResponseSerializer(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
def put(self, request: Request) -> Response:
|
||||||
|
"""Convert data into a blueprint, validate it and apply it"""
|
||||||
|
data = TransactionApplicationSerializer(data=request.data)
|
||||||
|
data.is_valid(raise_exception=True)
|
||||||
|
print(data.validated_data)
|
||||||
|
|
||||||
|
blueprint = Blueprint()
|
||||||
|
blueprint.entries.append(
|
||||||
|
BlueprintEntry(
|
||||||
|
model=data.validated_data["provider_model"],
|
||||||
|
state=BlueprintEntryDesiredState.PRESENT,
|
||||||
|
identifiers={
|
||||||
|
"name": data.validated_data["provider"]["name"],
|
||||||
|
},
|
||||||
|
id="provider",
|
||||||
|
attrs=data.validated_data["provider"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
app_data = data.validated_data["app"]
|
||||||
|
app_data["provider"] = KeyOf(None, ScalarNode(tag="", value="provider"))
|
||||||
|
blueprint.entries.append(
|
||||||
|
BlueprintEntry(
|
||||||
|
model="authentik_core.application",
|
||||||
|
state=BlueprintEntryDesiredState.PRESENT,
|
||||||
|
identifiers={
|
||||||
|
"slug": data.validated_data["app"]["slug"],
|
||||||
|
},
|
||||||
|
attrs=app_data,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
importer = Importer(blueprint, {})
|
||||||
|
response = {"valid": False, "applied": False, "logs": []}
|
||||||
|
valid, logs = importer.validate()
|
||||||
|
response["logs"] = [x["event"] for x in logs]
|
||||||
|
response["valid"] = valid
|
||||||
|
if valid:
|
||||||
|
applied = importer.apply()
|
||||||
|
response["applied"] = applied
|
||||||
|
return Response(response, status=200)
|
|
@ -0,0 +1,45 @@
|
||||||
|
"""Test Transactional API"""
|
||||||
|
from json import loads
|
||||||
|
|
||||||
|
from django.urls import reverse
|
||||||
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
|
from authentik.core.models import Application
|
||||||
|
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
|
||||||
|
from authentik.lib.generators import generate_id
|
||||||
|
from authentik.providers.oauth2.models import OAuth2Provider
|
||||||
|
|
||||||
|
|
||||||
|
class TestTransactionalApplicationsAPI(APITestCase):
|
||||||
|
"""Test Transactional API"""
|
||||||
|
|
||||||
|
def setUp(self) -> None:
|
||||||
|
self.user = create_test_admin_user()
|
||||||
|
|
||||||
|
def test_create_transactional(self):
|
||||||
|
"""Test transactional Application + provider creation"""
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
uid = generate_id()
|
||||||
|
authorization_flow = create_test_flow()
|
||||||
|
response = self.client.put(
|
||||||
|
reverse("authentik_api:core-transactional-application"),
|
||||||
|
data={
|
||||||
|
"app": {
|
||||||
|
"name": uid,
|
||||||
|
"slug": uid,
|
||||||
|
},
|
||||||
|
"provider_model": "authentik_providers_oauth2.oauth2provider",
|
||||||
|
"provider": {
|
||||||
|
"name": uid,
|
||||||
|
"authorization_flow": str(authorization_flow.pk),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
response_body = loads(response.content.decode())
|
||||||
|
self.assertTrue(response_body["valid"])
|
||||||
|
self.assertTrue(response_body["applied"])
|
||||||
|
provider = OAuth2Provider.objects.filter(name=uid).first()
|
||||||
|
self.assertIsNotNone(provider)
|
||||||
|
app = Application.objects.filter(slug=uid).first()
|
||||||
|
self.assertIsNotNone(app)
|
||||||
|
self.assertEqual(app.provider.pk, provider.pk)
|
|
@ -8,7 +8,6 @@ from django.views.decorators.csrf import ensure_csrf_cookie
|
||||||
from django.views.generic import RedirectView
|
from django.views.generic import RedirectView
|
||||||
|
|
||||||
from authentik.core.api.applications import ApplicationViewSet
|
from authentik.core.api.applications import ApplicationViewSet
|
||||||
from authentik.core.api.applications_transactional import TransactionalApplicationView
|
|
||||||
from authentik.core.api.authenticated_sessions import AuthenticatedSessionViewSet
|
from authentik.core.api.authenticated_sessions import AuthenticatedSessionViewSet
|
||||||
from authentik.core.api.devices import AdminDeviceViewSet, DeviceViewSet
|
from authentik.core.api.devices import AdminDeviceViewSet, DeviceViewSet
|
||||||
from authentik.core.api.groups import GroupViewSet
|
from authentik.core.api.groups import GroupViewSet
|
||||||
|
@ -16,6 +15,7 @@ from authentik.core.api.propertymappings import PropertyMappingViewSet
|
||||||
from authentik.core.api.providers import ProviderViewSet
|
from authentik.core.api.providers import ProviderViewSet
|
||||||
from authentik.core.api.sources import SourceViewSet, UserSourceConnectionViewSet
|
from authentik.core.api.sources import SourceViewSet, UserSourceConnectionViewSet
|
||||||
from authentik.core.api.tokens import TokenViewSet
|
from authentik.core.api.tokens import TokenViewSet
|
||||||
|
from authentik.core.api.transactional_applications import TransactionalApplicationView
|
||||||
from authentik.core.api.users import UserViewSet
|
from authentik.core.api.users import UserViewSet
|
||||||
from authentik.core.views import apps
|
from authentik.core.views import apps
|
||||||
from authentik.core.views.debug import AccessDeniedView
|
from authentik.core.views.debug import AccessDeniedView
|
||||||
|
@ -72,9 +72,9 @@ api_urlpatterns = [
|
||||||
("core/authenticated_sessions", AuthenticatedSessionViewSet),
|
("core/authenticated_sessions", AuthenticatedSessionViewSet),
|
||||||
("core/applications", ApplicationViewSet),
|
("core/applications", ApplicationViewSet),
|
||||||
path(
|
path(
|
||||||
"core/applications/create_transactional/",
|
"core/transactional/applications/",
|
||||||
TransactionalApplicationView.as_view(),
|
TransactionalApplicationView.as_view(),
|
||||||
name="core-apps-transactional",
|
name="core-transactional-application",
|
||||||
),
|
),
|
||||||
("core/groups", GroupViewSet),
|
("core/groups", GroupViewSet),
|
||||||
("core/users", UserViewSet),
|
("core/users", UserViewSet),
|
||||||
|
|
23
schema.yml
23
schema.yml
|
@ -3091,6 +3091,7 @@ paths:
|
||||||
/core/applications/create_transactional/:
|
/core/applications/create_transactional/:
|
||||||
put:
|
put:
|
||||||
operationId: core_applications_create_transactional_update
|
operationId: core_applications_create_transactional_update
|
||||||
|
description: Convert data into a blueprint, validate it and apply it
|
||||||
tags:
|
tags:
|
||||||
- core
|
- core
|
||||||
requestBody:
|
requestBody:
|
||||||
|
@ -3103,7 +3104,11 @@ paths:
|
||||||
- authentik: []
|
- authentik: []
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: No response body
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/TransactionApplicationResponse'
|
||||||
|
description: ''
|
||||||
'400':
|
'400':
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
|
@ -39971,6 +39976,22 @@ components:
|
||||||
- app
|
- app
|
||||||
- provider
|
- provider
|
||||||
- provider_model
|
- provider_model
|
||||||
|
TransactionApplicationResponse:
|
||||||
|
type: object
|
||||||
|
description: Transactional creation response
|
||||||
|
properties:
|
||||||
|
valid:
|
||||||
|
type: boolean
|
||||||
|
applied:
|
||||||
|
type: boolean
|
||||||
|
logs:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- applied
|
||||||
|
- logs
|
||||||
|
- valid
|
||||||
TypeCreate:
|
TypeCreate:
|
||||||
type: object
|
type: object
|
||||||
description: Types of an object that can be created
|
description: Types of an object that can be created
|
||||||
|
|
Reference in New Issue