blueprints: add generic export next to flow exporter (#3439)

This commit is contained in:
Jens L 2022-08-17 17:57:59 +01:00 committed by GitHub
parent 846b63a17b
commit e87236b285
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 93 additions and 33 deletions

View File

@ -0,0 +1,17 @@
"""Export blueprint of current authentik install"""
from django.core.management.base import BaseCommand, no_translations
from structlog.stdlib import get_logger
from authentik.blueprints.v1.exporter import Exporter
LOGGER = get_logger()
class Command(BaseCommand):
"""Export blueprint of current authentik install"""
@no_translations
def handle(self, *args, **options):
"""Export blueprint of current authentik install"""
exporter = Exporter()
self.stdout.write(exporter.export_to_string())

View File

@ -1,10 +1,8 @@
"""test packaged blueprints""" """test packaged blueprints"""
from glob import glob
from pathlib import Path from pathlib import Path
from typing import Callable from typing import Callable
from django.test import TransactionTestCase from django.test import TransactionTestCase
from django.utils.text import slugify
from authentik.blueprints.tests import apply_blueprint from authentik.blueprints.tests import apply_blueprint
from authentik.blueprints.v1.importer import Importer from authentik.blueprints.v1.importer import Importer
@ -24,14 +22,13 @@ def blueprint_tester(file_name: str) -> Callable:
"""This is used instead of subTest for better visibility""" """This is used instead of subTest for better visibility"""
def tester(self: TestBundled): def tester(self: TestBundled):
with open(file_name, "r", encoding="utf8") as flow_yaml: with open(file_name, "r", encoding="utf8") as blueprint:
importer = Importer(flow_yaml.read()) importer = Importer(blueprint.read())
self.assertTrue(importer.validate()[0]) self.assertTrue(importer.validate()[0])
self.assertTrue(importer.apply()) self.assertTrue(importer.apply())
return tester return tester
for flow_file in glob("blueprints/**/*.yaml", recursive=True): for blueprint_file in Path("blueprints/").glob("**/*.yaml"):
method_name = slugify(Path(flow_file).stem).replace("-", "_").replace(".", "_") setattr(TestBundled, f"test_blueprint_{blueprint_file}", blueprint_tester(blueprint_file))
setattr(TestBundled, f"test_flow_{method_name}", blueprint_tester(flow_file))

View File

@ -1,7 +1,7 @@
"""Test blueprints v1""" """Test blueprints v1"""
from django.test import TransactionTestCase from django.test import TransactionTestCase
from authentik.blueprints.v1.exporter import Exporter from authentik.blueprints.v1.exporter import FlowExporter
from authentik.blueprints.v1.importer import Importer, transaction_rollback from authentik.blueprints.v1.importer import Importer, transaction_rollback
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding
from authentik.lib.generators import generate_id from authentik.lib.generators import generate_id
@ -70,7 +70,7 @@ class TestBlueprintsV1(TransactionTestCase):
order=0, order=0,
) )
exporter = Exporter(flow) exporter = FlowExporter(flow)
export = exporter.export() export = exporter.export()
self.assertEqual(len(export.entries), 3) self.assertEqual(len(export.entries), 3)
export_yaml = exporter.export_to_string() export_yaml = exporter.export_to_string()
@ -126,7 +126,7 @@ class TestBlueprintsV1(TransactionTestCase):
fsb = FlowStageBinding.objects.create(target=flow, stage=user_login, order=0) fsb = FlowStageBinding.objects.create(target=flow, stage=user_login, order=0)
PolicyBinding.objects.create(policy=flow_policy, target=fsb, order=0) PolicyBinding.objects.create(policy=flow_policy, target=fsb, order=0)
exporter = Exporter(flow) exporter = FlowExporter(flow)
export_yaml = exporter.export_to_string() export_yaml = exporter.export_to_string()
importer = Importer(export_yaml) importer = Importer(export_yaml)
@ -169,7 +169,7 @@ class TestBlueprintsV1(TransactionTestCase):
FlowStageBinding.objects.create(target=flow, stage=first_stage, order=0) FlowStageBinding.objects.create(target=flow, stage=first_stage, order=0)
exporter = Exporter(flow) exporter = FlowExporter(flow)
export_yaml = exporter.export_to_string() export_yaml = exporter.export_to_string()
importer = Importer(export_yaml) importer = Importer(export_yaml)

View File

@ -1,11 +1,21 @@
"""Flow exporter""" """Blueprint exporter"""
from typing import Iterator from typing import Iterator
from uuid import UUID from uuid import UUID
from django.apps import apps
from django.db.models import Q from django.db.models import Q
from django.utils.timezone import now
from django.utils.translation import gettext as _
from yaml import dump from yaml import dump
from authentik.blueprints.v1.common import Blueprint, BlueprintDumper, BlueprintEntry from authentik.blueprints.v1.common import (
Blueprint,
BlueprintDumper,
BlueprintEntry,
BlueprintMetadata,
)
from authentik.blueprints.v1.importer import is_model_allowed
from authentik.blueprints.v1.labels import LABEL_AUTHENTIK_GENERATED
from authentik.flows.models import Flow, FlowStageBinding, Stage from authentik.flows.models import Flow, FlowStageBinding, Stage
from authentik.policies.models import Policy, PolicyBinding from authentik.policies.models import Policy, PolicyBinding
from authentik.stages.prompt.models import PromptStage from authentik.stages.prompt.models import PromptStage
@ -14,6 +24,46 @@ from authentik.stages.prompt.models import PromptStage
class Exporter: class Exporter:
"""Export flow with attached stages into yaml""" """Export flow with attached stages into yaml"""
excluded_models = []
def __init__(self):
self.excluded_models = []
def get_entries(self) -> Iterator[BlueprintEntry]:
"""Get blueprint entries"""
for model in apps.get_models():
if not is_model_allowed(model):
continue
if model in self.excluded_models:
continue
for obj in model.objects.all():
yield BlueprintEntry.from_model(obj)
def _pre_export(self, blueprint: Blueprint):
"""Hook to run anything pre-export"""
def export(self) -> Blueprint:
"""Create a list of all objects and create a blueprint"""
blueprint = Blueprint()
self._pre_export(blueprint)
blueprint.metadata = BlueprintMetadata(
name=_("authentik Export - %(date)s" % {"date": str(now())}),
labels={
LABEL_AUTHENTIK_GENERATED: "true",
},
)
blueprint.entries = list(self.get_entries())
return blueprint
def export_to_string(self) -> str:
"""Call export and convert it to yaml"""
blueprint = self.export()
return dump(blueprint, Dumper=BlueprintDumper)
class FlowExporter(Exporter):
"""Exporter customised to only return objects related to `flow`"""
flow: Flow flow: Flow
with_policies: bool with_policies: bool
with_stage_prompts: bool with_stage_prompts: bool
@ -21,11 +71,14 @@ class Exporter:
pbm_uuids: list[UUID] pbm_uuids: list[UUID]
def __init__(self, flow: Flow): def __init__(self, flow: Flow):
super().__init__()
self.flow = flow self.flow = flow
self.with_policies = True self.with_policies = True
self.with_stage_prompts = True self.with_stage_prompts = True
def _prepare_pbm(self): def _pre_export(self, blueprint: Blueprint):
if not self.with_policies:
return
self.pbm_uuids = [self.flow.pbm_uuid] self.pbm_uuids = [self.flow.pbm_uuid]
self.pbm_uuids += FlowStageBinding.objects.filter(target=self.flow).values_list( self.pbm_uuids += FlowStageBinding.objects.filter(target=self.flow).values_list(
"pbm_uuid", flat=True "pbm_uuid", flat=True
@ -70,23 +123,15 @@ class Exporter:
for prompt in stage.fields.all(): for prompt in stage.fields.all():
yield BlueprintEntry.from_model(prompt) yield BlueprintEntry.from_model(prompt)
def export(self) -> Blueprint: def get_entries(self) -> Iterator[BlueprintEntry]:
"""Create a list of all objects including the flow""" entries = []
if self.with_policies: entries.append(BlueprintEntry.from_model(self.flow, "slug"))
self._prepare_pbm()
bundle = Blueprint()
bundle.entries.append(BlueprintEntry.from_model(self.flow, "slug"))
if self.with_stage_prompts: if self.with_stage_prompts:
bundle.entries.extend(self.walk_stage_prompts()) entries.extend(self.walk_stage_prompts())
if self.with_policies: if self.with_policies:
bundle.entries.extend(self.walk_policies()) entries.extend(self.walk_policies())
bundle.entries.extend(self.walk_stages()) entries.extend(self.walk_stages())
bundle.entries.extend(self.walk_stage_bindings()) entries.extend(self.walk_stage_bindings())
if self.with_policies: if self.with_policies:
bundle.entries.extend(self.walk_policy_bindings()) entries.extend(self.walk_policy_bindings())
return bundle return entries
def export_to_string(self) -> str:
"""Call export and convert it to yaml"""
bundle = self.export()
return dump(bundle, Dumper=BlueprintDumper)

View File

@ -2,3 +2,4 @@
LABEL_AUTHENTIK_SYSTEM = "blueprints.goauthentik.io/system" LABEL_AUTHENTIK_SYSTEM = "blueprints.goauthentik.io/system"
LABEL_AUTHENTIK_INSTANTIATE = "blueprints.goauthentik.io/instantiate" LABEL_AUTHENTIK_INSTANTIATE = "blueprints.goauthentik.io/instantiate"
LABEL_AUTHENTIK_GENERATED = "blueprints.goauthentik.io/generated"

View File

@ -20,7 +20,7 @@ from rest_framework.viewsets import ModelViewSet
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from authentik.api.decorators import permission_required from authentik.api.decorators import permission_required
from authentik.blueprints.v1.exporter import Exporter from authentik.blueprints.v1.exporter import FlowExporter
from authentik.blueprints.v1.importer import Importer from authentik.blueprints.v1.importer import Importer
from authentik.core.api.used_by import UsedByMixin from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ( from authentik.core.api.utils import (
@ -198,7 +198,7 @@ class FlowViewSet(UsedByMixin, ModelViewSet):
def export(self, request: Request, slug: str) -> Response: def export(self, request: Request, slug: str) -> Response:
"""Export flow to .yaml file""" """Export flow to .yaml file"""
flow = self.get_object() flow = self.get_object()
exporter = Exporter(flow) exporter = FlowExporter(flow)
response = HttpResponse(content=exporter.export_to_string()) response = HttpResponse(content=exporter.export_to_string())
response["Content-Disposition"] = f'attachment; filename="{flow.slug}.yaml"' response["Content-Disposition"] = f'attachment; filename="{flow.slug}.yaml"'
return response return response