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"""
from glob import glob
from pathlib import Path
from typing import Callable
from django.test import TransactionTestCase
from django.utils.text import slugify
from authentik.blueprints.tests import apply_blueprint
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"""
def tester(self: TestBundled):
with open(file_name, "r", encoding="utf8") as flow_yaml:
importer = Importer(flow_yaml.read())
with open(file_name, "r", encoding="utf8") as blueprint:
importer = Importer(blueprint.read())
self.assertTrue(importer.validate()[0])
self.assertTrue(importer.apply())
return tester
for flow_file in glob("blueprints/**/*.yaml", recursive=True):
method_name = slugify(Path(flow_file).stem).replace("-", "_").replace(".", "_")
setattr(TestBundled, f"test_flow_{method_name}", blueprint_tester(flow_file))
for blueprint_file in Path("blueprints/").glob("**/*.yaml"):
setattr(TestBundled, f"test_blueprint_{blueprint_file}", blueprint_tester(blueprint_file))

View file

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

View file

@ -1,11 +1,21 @@
"""Flow exporter"""
"""Blueprint exporter"""
from typing import Iterator
from uuid import UUID
from django.apps import apps
from django.db.models import Q
from django.utils.timezone import now
from django.utils.translation import gettext as _
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.policies.models import Policy, PolicyBinding
from authentik.stages.prompt.models import PromptStage
@ -14,6 +24,46 @@ from authentik.stages.prompt.models import PromptStage
class Exporter:
"""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
with_policies: bool
with_stage_prompts: bool
@ -21,11 +71,14 @@ class Exporter:
pbm_uuids: list[UUID]
def __init__(self, flow: Flow):
super().__init__()
self.flow = flow
self.with_policies = 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 += FlowStageBinding.objects.filter(target=self.flow).values_list(
"pbm_uuid", flat=True
@ -70,23 +123,15 @@ class Exporter:
for prompt in stage.fields.all():
yield BlueprintEntry.from_model(prompt)
def export(self) -> Blueprint:
"""Create a list of all objects including the flow"""
if self.with_policies:
self._prepare_pbm()
bundle = Blueprint()
bundle.entries.append(BlueprintEntry.from_model(self.flow, "slug"))
def get_entries(self) -> Iterator[BlueprintEntry]:
entries = []
entries.append(BlueprintEntry.from_model(self.flow, "slug"))
if self.with_stage_prompts:
bundle.entries.extend(self.walk_stage_prompts())
entries.extend(self.walk_stage_prompts())
if self.with_policies:
bundle.entries.extend(self.walk_policies())
bundle.entries.extend(self.walk_stages())
bundle.entries.extend(self.walk_stage_bindings())
entries.extend(self.walk_policies())
entries.extend(self.walk_stages())
entries.extend(self.walk_stage_bindings())
if self.with_policies:
bundle.entries.extend(self.walk_policy_bindings())
return bundle
def export_to_string(self) -> str:
"""Call export and convert it to yaml"""
bundle = self.export()
return dump(bundle, Dumper=BlueprintDumper)
entries.extend(self.walk_policy_bindings())
return entries

View file

@ -2,3 +2,4 @@
LABEL_AUTHENTIK_SYSTEM = "blueprints.goauthentik.io/system"
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 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.core.api.used_by import UsedByMixin
from authentik.core.api.utils import (
@ -198,7 +198,7 @@ class FlowViewSet(UsedByMixin, ModelViewSet):
def export(self, request: Request, slug: str) -> Response:
"""Export flow to .yaml file"""
flow = self.get_object()
exporter = Exporter(flow)
exporter = FlowExporter(flow)
response = HttpResponse(content=exporter.export_to_string())
response["Content-Disposition"] = f'attachment; filename="{flow.slug}.yaml"'
return response