blueprints: webui (#3356)
This commit is contained in:
parent
20aeed139d
commit
d1004e3798
9
Makefile
9
Makefile
|
@ -172,3 +172,12 @@ ci-pending-migrations: ci--meta-debug
|
||||||
|
|
||||||
install: web-install website-install
|
install: web-install website-install
|
||||||
poetry install
|
poetry install
|
||||||
|
|
||||||
|
dev-reset:
|
||||||
|
dropdb -U postgres -h localhost authentik
|
||||||
|
createdb -U postgres -h localhost authentik
|
||||||
|
redis-cli -n 0 flushall
|
||||||
|
redis-cli -n 1 flushall
|
||||||
|
redis-cli -n 2 flushall
|
||||||
|
redis-cli -n 3 flushall
|
||||||
|
make migrate
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
"""test admin api"""
|
"""test admin api"""
|
||||||
from json import loads
|
from json import loads
|
||||||
|
|
||||||
from django.apps import apps
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from authentik import __version__
|
from authentik import __version__
|
||||||
|
from authentik.blueprints.tests import reconcile_app
|
||||||
from authentik.core.models import Group, User
|
from authentik.core.models import Group, User
|
||||||
from authentik.core.tasks import clean_expired_models
|
from authentik.core.tasks import clean_expired_models
|
||||||
from authentik.events.monitored_tasks import TaskResultStatus
|
from authentik.events.monitored_tasks import TaskResultStatus
|
||||||
|
@ -93,8 +93,8 @@ class TestAdminAPI(TestCase):
|
||||||
response = self.client.get(reverse("authentik_api:apps-list"))
|
response = self.client.get(reverse("authentik_api:apps-list"))
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
@reconcile_app("authentik_outposts")
|
||||||
def test_system(self):
|
def test_system(self):
|
||||||
"""Test system API"""
|
"""Test system API"""
|
||||||
apps.get_app_config("authentik_outposts").reconcile_embedded_outpost()
|
|
||||||
response = self.client.get(reverse("authentik_api:admin_system"))
|
response = self.client.get(reverse("authentik_api:admin_system"))
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
"""Test API Authentication"""
|
"""Test API Authentication"""
|
||||||
from base64 import b64encode
|
from base64 import b64encode
|
||||||
|
|
||||||
from django.apps import apps
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from guardian.shortcuts import get_anonymous_user
|
from guardian.shortcuts import get_anonymous_user
|
||||||
from rest_framework.exceptions import AuthenticationFailed
|
from rest_framework.exceptions import AuthenticationFailed
|
||||||
|
|
||||||
from authentik.api.authentication import bearer_auth
|
from authentik.api.authentication import bearer_auth
|
||||||
|
from authentik.blueprints.tests import reconcile_app
|
||||||
from authentik.core.models import USER_ATTRIBUTE_SA, Token, TokenIntents
|
from authentik.core.models import USER_ATTRIBUTE_SA, Token, TokenIntents
|
||||||
from authentik.core.tests.utils import create_test_flow
|
from authentik.core.tests.utils import create_test_flow
|
||||||
from authentik.lib.generators import generate_id
|
from authentik.lib.generators import generate_id
|
||||||
|
@ -42,9 +42,11 @@ class TestAPIAuth(TestCase):
|
||||||
def test_managed_outpost(self):
|
def test_managed_outpost(self):
|
||||||
"""Test managed outpost"""
|
"""Test managed outpost"""
|
||||||
with self.assertRaises(AuthenticationFailed):
|
with self.assertRaises(AuthenticationFailed):
|
||||||
user = bearer_auth(f"Bearer {settings.SECRET_KEY}".encode())
|
bearer_auth(f"Bearer {settings.SECRET_KEY}".encode())
|
||||||
|
|
||||||
apps.get_app_config("authentik_outposts").reconcile_embedded_outpost()
|
@reconcile_app("authentik_outposts")
|
||||||
|
def test_managed_outpost_success(self):
|
||||||
|
"""Test managed outpost"""
|
||||||
user = bearer_auth(f"Bearer {settings.SECRET_KEY}".encode())
|
user = bearer_auth(f"Bearer {settings.SECRET_KEY}".encode())
|
||||||
self.assertEqual(user.attributes[USER_ATTRIBUTE_SA], True)
|
self.assertEqual(user.attributes[USER_ATTRIBUTE_SA], True)
|
||||||
|
|
||||||
|
|
|
@ -1,23 +0,0 @@
|
||||||
"""Blueprint helpers"""
|
|
||||||
from functools import wraps
|
|
||||||
from typing import Callable
|
|
||||||
|
|
||||||
|
|
||||||
def apply_blueprint(*files: str):
|
|
||||||
"""Apply blueprint before test"""
|
|
||||||
|
|
||||||
from authentik.blueprints.v1.importer import Importer
|
|
||||||
|
|
||||||
def wrapper_outer(func: Callable):
|
|
||||||
"""Apply blueprint before test"""
|
|
||||||
|
|
||||||
@wraps(func)
|
|
||||||
def wrapper(*args, **kwargs):
|
|
||||||
for file in files:
|
|
||||||
with open(file, "r+", encoding="utf-8") as _file:
|
|
||||||
Importer(_file.read()).apply()
|
|
||||||
return func(*args, **kwargs)
|
|
||||||
|
|
||||||
return wrapper
|
|
||||||
|
|
||||||
return wrapper_outer
|
|
|
@ -1,17 +1,20 @@
|
||||||
"""Serializer mixin for managed models"""
|
"""Serializer mixin for managed models"""
|
||||||
from glob import glob
|
from dataclasses import asdict
|
||||||
|
|
||||||
from drf_spectacular.utils import extend_schema
|
from drf_spectacular.utils import extend_schema, inline_serializer
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.fields import CharField
|
from rest_framework.fields import CharField, DateTimeField, 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
|
||||||
from rest_framework.serializers import ListSerializer, ModelSerializer
|
from rest_framework.serializers import ListSerializer, ModelSerializer
|
||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
|
from authentik.api.decorators import permission_required
|
||||||
from authentik.blueprints.models import BlueprintInstance
|
from authentik.blueprints.models import BlueprintInstance
|
||||||
from authentik.lib.config import CONFIG
|
from authentik.blueprints.v1.tasks import BlueprintFile, apply_blueprint, blueprints_find
|
||||||
|
from authentik.core.api.used_by import UsedByMixin
|
||||||
|
from authentik.core.api.utils import PassiveSerializer
|
||||||
|
|
||||||
|
|
||||||
class ManagedSerializer:
|
class ManagedSerializer:
|
||||||
|
@ -20,6 +23,13 @@ class ManagedSerializer:
|
||||||
managed = CharField(read_only=True, allow_null=True)
|
managed = CharField(read_only=True, allow_null=True)
|
||||||
|
|
||||||
|
|
||||||
|
class MetadataSerializer(PassiveSerializer):
|
||||||
|
"""Serializer for blueprint metadata"""
|
||||||
|
|
||||||
|
name = CharField()
|
||||||
|
labels = JSONField()
|
||||||
|
|
||||||
|
|
||||||
class BlueprintInstanceSerializer(ModelSerializer):
|
class BlueprintInstanceSerializer(ModelSerializer):
|
||||||
"""Info about a single blueprint instance file"""
|
"""Info about a single blueprint instance file"""
|
||||||
|
|
||||||
|
@ -36,15 +46,18 @@ class BlueprintInstanceSerializer(ModelSerializer):
|
||||||
"status",
|
"status",
|
||||||
"enabled",
|
"enabled",
|
||||||
"managed_models",
|
"managed_models",
|
||||||
|
"metadata",
|
||||||
]
|
]
|
||||||
extra_kwargs = {
|
extra_kwargs = {
|
||||||
|
"status": {"read_only": True},
|
||||||
"last_applied": {"read_only": True},
|
"last_applied": {"read_only": True},
|
||||||
"last_applied_hash": {"read_only": True},
|
"last_applied_hash": {"read_only": True},
|
||||||
"managed_models": {"read_only": True},
|
"managed_models": {"read_only": True},
|
||||||
|
"metadata": {"read_only": True},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class BlueprintInstanceViewSet(ModelViewSet):
|
class BlueprintInstanceViewSet(UsedByMixin, ModelViewSet):
|
||||||
"""Blueprint instances"""
|
"""Blueprint instances"""
|
||||||
|
|
||||||
permission_classes = [IsAdminUser]
|
permission_classes = [IsAdminUser]
|
||||||
|
@ -53,12 +66,37 @@ class BlueprintInstanceViewSet(ModelViewSet):
|
||||||
search_fields = ["name", "path"]
|
search_fields = ["name", "path"]
|
||||||
filterset_fields = ["name", "path"]
|
filterset_fields = ["name", "path"]
|
||||||
|
|
||||||
@extend_schema(responses={200: ListSerializer(child=CharField())})
|
@extend_schema(
|
||||||
|
responses={
|
||||||
|
200: ListSerializer(
|
||||||
|
child=inline_serializer(
|
||||||
|
"BlueprintFile",
|
||||||
|
fields={
|
||||||
|
"path": CharField(),
|
||||||
|
"last_m": DateTimeField(),
|
||||||
|
"hash": CharField(),
|
||||||
|
"meta": MetadataSerializer(required=False, read_only=True),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
@action(detail=False, pagination_class=None, filter_backends=[])
|
@action(detail=False, pagination_class=None, filter_backends=[])
|
||||||
def available(self, request: Request) -> Response:
|
def available(self, request: Request) -> Response:
|
||||||
"""Get blueprints"""
|
"""Get blueprints"""
|
||||||
files = []
|
files: list[BlueprintFile] = blueprints_find.delay().get()
|
||||||
for folder in CONFIG.y("blueprint_locations"):
|
return Response([asdict(file) for file in files])
|
||||||
for file in glob(f"{folder}/**", recursive=True):
|
|
||||||
files.append(file)
|
@permission_required("authentik_blueprints.view_blueprintinstance")
|
||||||
return Response(files)
|
@extend_schema(
|
||||||
|
request=None,
|
||||||
|
responses={
|
||||||
|
200: BlueprintInstanceSerializer(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
@action(detail=True, pagination_class=None, filter_backends=[], methods=["POST"])
|
||||||
|
def apply(self, request: Request, *args, **kwargs) -> Response:
|
||||||
|
"""Apply a blueprint"""
|
||||||
|
blueprint = self.get_object()
|
||||||
|
apply_blueprint.delay(str(blueprint.pk)).get()
|
||||||
|
return self.retrieve(request, *args, **kwargs)
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
"""Apply blueprint from commandline"""
|
"""Apply blueprint from commandline"""
|
||||||
from django.core.management.base import BaseCommand, no_translations
|
from django.core.management.base import BaseCommand, no_translations
|
||||||
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
from authentik.blueprints.v1.importer import Importer
|
from authentik.blueprints.v1.importer import Importer
|
||||||
|
|
||||||
|
LOGGER = get_logger()
|
||||||
|
|
||||||
class Command(BaseCommand): # pragma: no cover
|
|
||||||
|
class Command(BaseCommand):
|
||||||
"""Apply blueprint from commandline"""
|
"""Apply blueprint from commandline"""
|
||||||
|
|
||||||
@no_translations
|
@no_translations
|
||||||
|
@ -15,7 +18,9 @@ class Command(BaseCommand): # pragma: no cover
|
||||||
importer = Importer(blueprint_file.read())
|
importer = Importer(blueprint_file.read())
|
||||||
valid, logs = importer.validate()
|
valid, logs = importer.validate()
|
||||||
if not valid:
|
if not valid:
|
||||||
raise ValueError(f"blueprint invalid: {logs}")
|
for log in logs:
|
||||||
|
LOGGER.debug(**log)
|
||||||
|
raise ValueError("blueprint invalid")
|
||||||
importer.apply()
|
importer.apply()
|
||||||
|
|
||||||
def add_arguments(self, parser):
|
def add_arguments(self, parser):
|
||||||
|
|
|
@ -4,7 +4,7 @@ from inspect import ismethod
|
||||||
|
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
from django.db import DatabaseError, InternalError, ProgrammingError
|
from django.db import DatabaseError, InternalError, ProgrammingError
|
||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import BoundLogger, get_logger
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
|
||||||
|
@ -12,6 +12,12 @@ LOGGER = get_logger()
|
||||||
class ManagedAppConfig(AppConfig):
|
class ManagedAppConfig(AppConfig):
|
||||||
"""Basic reconciliation logic for apps"""
|
"""Basic reconciliation logic for apps"""
|
||||||
|
|
||||||
|
_logger: BoundLogger
|
||||||
|
|
||||||
|
def __init__(self, app_name: str, *args, **kwargs) -> None:
|
||||||
|
super().__init__(app_name, *args, **kwargs)
|
||||||
|
self._logger = get_logger().bind(app_name=app_name)
|
||||||
|
|
||||||
def ready(self) -> None:
|
def ready(self) -> None:
|
||||||
self.reconcile()
|
self.reconcile()
|
||||||
return super().ready()
|
return super().ready()
|
||||||
|
@ -31,7 +37,8 @@ class ManagedAppConfig(AppConfig):
|
||||||
continue
|
continue
|
||||||
name = meth_name.replace(prefix, "")
|
name = meth_name.replace(prefix, "")
|
||||||
try:
|
try:
|
||||||
|
self._logger.debug("Starting reconciler", name=name)
|
||||||
meth()
|
meth()
|
||||||
LOGGER.debug("Successfully reconciled", name=name)
|
self._logger.debug("Successfully reconciled", name=name)
|
||||||
except (DatabaseError, ProgrammingError, InternalError) as exc:
|
except (DatabaseError, ProgrammingError, InternalError) as exc:
|
||||||
LOGGER.debug("Failed to run reconcile", name=name, exc=exc)
|
self._logger.debug("Failed to run reconcile", name=name, exc=exc)
|
||||||
|
|
|
@ -4,7 +4,9 @@ from glob import glob
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import django.contrib.postgres.fields
|
import django.contrib.postgres.fields
|
||||||
|
from dacite import from_dict
|
||||||
from django.apps.registry import Apps
|
from django.apps.registry import Apps
|
||||||
|
from django.conf import settings
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||||
from yaml import load
|
from yaml import load
|
||||||
|
@ -15,24 +17,33 @@ from authentik.lib.config import CONFIG
|
||||||
def check_blueprint_v1_file(BlueprintInstance: type["BlueprintInstance"], path: Path):
|
def check_blueprint_v1_file(BlueprintInstance: type["BlueprintInstance"], path: Path):
|
||||||
"""Check if blueprint should be imported"""
|
"""Check if blueprint should be imported"""
|
||||||
from authentik.blueprints.models import BlueprintInstanceStatus
|
from authentik.blueprints.models import BlueprintInstanceStatus
|
||||||
from authentik.blueprints.v1.common import BlueprintLoader
|
from authentik.blueprints.v1.common import BlueprintLoader, BlueprintMetadata
|
||||||
|
from authentik.blueprints.v1.labels import LABEL_AUTHENTIK_EXAMPLE
|
||||||
|
|
||||||
with open(path, "r", encoding="utf-8") as blueprint_file:
|
with open(path, "r", encoding="utf-8") as blueprint_file:
|
||||||
raw_blueprint = load(blueprint_file.read(), BlueprintLoader)
|
raw_blueprint = load(blueprint_file.read(), BlueprintLoader)
|
||||||
|
metadata = raw_blueprint.get("metadata", None)
|
||||||
version = raw_blueprint.get("version", 1)
|
version = raw_blueprint.get("version", 1)
|
||||||
if version != 1:
|
if version != 1:
|
||||||
return
|
return
|
||||||
blueprint_file.seek(0)
|
blueprint_file.seek(0)
|
||||||
instance: BlueprintInstance = BlueprintInstance.objects.filter(path=path).first()
|
instance: BlueprintInstance = BlueprintInstance.objects.filter(path=path).first()
|
||||||
|
rel_path = path.relative_to(Path(CONFIG.y("blueprints_dir")))
|
||||||
|
meta = None
|
||||||
|
if metadata:
|
||||||
|
meta = from_dict(BlueprintMetadata, metadata)
|
||||||
|
if meta.labels.get(LABEL_AUTHENTIK_EXAMPLE, "").lower() == "true":
|
||||||
|
return
|
||||||
if not instance:
|
if not instance:
|
||||||
instance = BlueprintInstance(
|
instance = BlueprintInstance(
|
||||||
name=path.name,
|
name=meta.name if meta else str(rel_path),
|
||||||
path=str(path),
|
path=str(path),
|
||||||
context={},
|
context={},
|
||||||
status=BlueprintInstanceStatus.UNKNOWN,
|
status=BlueprintInstanceStatus.UNKNOWN,
|
||||||
enabled=True,
|
enabled=True,
|
||||||
managed_models=[],
|
managed_models=[],
|
||||||
last_applied_hash="",
|
last_applied_hash="",
|
||||||
|
metadata=metadata,
|
||||||
)
|
)
|
||||||
instance.save()
|
instance.save()
|
||||||
|
|
||||||
|
@ -42,9 +53,8 @@ def migration_blueprint_import(apps: Apps, schema_editor: BaseDatabaseSchemaEdit
|
||||||
Flow = apps.get_model("authentik_flows", "Flow")
|
Flow = apps.get_model("authentik_flows", "Flow")
|
||||||
|
|
||||||
db_alias = schema_editor.connection.alias
|
db_alias = schema_editor.connection.alias
|
||||||
for folder in CONFIG.y("blueprint_locations"):
|
for file in glob(f"{CONFIG.y('blueprints_dir')}/**/*.yaml", recursive=True):
|
||||||
for file in glob(f"{folder}/**/*.yaml", recursive=True):
|
check_blueprint_v1_file(BlueprintInstance, Path(file))
|
||||||
check_blueprint_v1_file(BlueprintInstance, Path(file))
|
|
||||||
|
|
||||||
for blueprint in BlueprintInstance.objects.using(db_alias).all():
|
for blueprint in BlueprintInstance.objects.using(db_alias).all():
|
||||||
# If we already have flows (and we should always run before flow migrations)
|
# If we already have flows (and we should always run before flow migrations)
|
||||||
|
@ -86,8 +96,9 @@ class Migration(migrations.Migration):
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
("name", models.TextField()),
|
("name", models.TextField()),
|
||||||
|
("metadata", models.JSONField(default=dict)),
|
||||||
("path", models.TextField()),
|
("path", models.TextField()),
|
||||||
("context", models.JSONField()),
|
("context", models.JSONField(default=dict)),
|
||||||
("last_applied", models.DateTimeField(auto_now=True)),
|
("last_applied", models.DateTimeField(auto_now=True)),
|
||||||
("last_applied_hash", models.TextField()),
|
("last_applied_hash", models.TextField()),
|
||||||
(
|
(
|
||||||
|
@ -106,7 +117,7 @@ class Migration(migrations.Migration):
|
||||||
(
|
(
|
||||||
"managed_models",
|
"managed_models",
|
||||||
django.contrib.postgres.fields.ArrayField(
|
django.contrib.postgres.fields.ArrayField(
|
||||||
base_field=models.TextField(), size=None
|
base_field=models.TextField(), default=list, size=None
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
@ -49,13 +49,14 @@ class BlueprintInstance(SerializerModel, ManagedModel, CreatedUpdatedModel):
|
||||||
instance_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4)
|
instance_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4)
|
||||||
|
|
||||||
name = models.TextField()
|
name = models.TextField()
|
||||||
|
metadata = models.JSONField(default=dict)
|
||||||
path = models.TextField()
|
path = models.TextField()
|
||||||
context = models.JSONField()
|
context = models.JSONField(default=dict)
|
||||||
last_applied = models.DateTimeField(auto_now=True)
|
last_applied = models.DateTimeField(auto_now=True)
|
||||||
last_applied_hash = models.TextField()
|
last_applied_hash = models.TextField()
|
||||||
status = models.TextField(choices=BlueprintInstanceStatus.choices)
|
status = models.TextField(choices=BlueprintInstanceStatus.choices)
|
||||||
enabled = models.BooleanField(default=True)
|
enabled = models.BooleanField(default=True)
|
||||||
managed_models = ArrayField(models.TextField())
|
managed_models = ArrayField(models.TextField(), default=list)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def serializer(self) -> Serializer:
|
def serializer(self) -> Serializer:
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
"""Blueprint helpers"""
|
||||||
|
from functools import wraps
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
|
from django.apps import apps
|
||||||
|
|
||||||
|
from authentik.blueprints.manager import ManagedAppConfig
|
||||||
|
|
||||||
|
|
||||||
|
def apply_blueprint(*files: str):
|
||||||
|
"""Apply blueprint before test"""
|
||||||
|
|
||||||
|
from authentik.blueprints.v1.importer import Importer
|
||||||
|
|
||||||
|
def wrapper_outer(func: Callable):
|
||||||
|
"""Apply blueprint before test"""
|
||||||
|
|
||||||
|
@wraps(func)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
for file in files:
|
||||||
|
with open(file, "r+", encoding="utf-8") as _file:
|
||||||
|
Importer(_file.read()).apply()
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
return wrapper_outer
|
||||||
|
|
||||||
|
|
||||||
|
def reconcile_app(app_name: str):
|
||||||
|
"""Re-reconcile AppConfig methods"""
|
||||||
|
|
||||||
|
def wrapper_outer(func: Callable):
|
||||||
|
"""Re-reconcile AppConfig methods"""
|
||||||
|
|
||||||
|
@wraps(func)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
config = apps.get_app_config(app_name)
|
||||||
|
if isinstance(config, ManagedAppConfig):
|
||||||
|
config.reconcile()
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
return wrapper_outer
|
|
@ -6,12 +6,19 @@ from typing import Callable
|
||||||
from django.test import TransactionTestCase
|
from django.test import TransactionTestCase
|
||||||
from django.utils.text import slugify
|
from django.utils.text import slugify
|
||||||
|
|
||||||
|
from authentik.blueprints.tests import apply_blueprint
|
||||||
from authentik.blueprints.v1.importer import Importer
|
from authentik.blueprints.v1.importer import Importer
|
||||||
|
from authentik.tenants.models import Tenant
|
||||||
|
|
||||||
|
|
||||||
class TestBundled(TransactionTestCase):
|
class TestBundled(TransactionTestCase):
|
||||||
"""Empty class, test methods are added dynamically"""
|
"""Empty class, test methods are added dynamically"""
|
||||||
|
|
||||||
|
@apply_blueprint("blueprints/default/90-default-tenant.yaml")
|
||||||
|
def test_decorator_static(self):
|
||||||
|
"""Test @apply_blueprint decorator"""
|
||||||
|
self.assertTrue(Tenant.objects.filter(domain="authentik-default").exists())
|
||||||
|
|
||||||
|
|
||||||
def blueprint_tester(file_name: str) -> Callable:
|
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"""
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
"""Test flow Transport"""
|
"""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 Exporter
|
||||||
|
@ -10,32 +10,26 @@ from authentik.policies.models import PolicyBinding
|
||||||
from authentik.stages.prompt.models import FieldTypes, Prompt, PromptStage
|
from authentik.stages.prompt.models import FieldTypes, Prompt, PromptStage
|
||||||
from authentik.stages.user_login.models import UserLoginStage
|
from authentik.stages.user_login.models import UserLoginStage
|
||||||
|
|
||||||
STATIC_PROMPT_EXPORT = """{
|
STATIC_PROMPT_EXPORT = """version: 1
|
||||||
"version": 1,
|
entries:
|
||||||
"entries": [
|
- identifiers:
|
||||||
{
|
pk: cb954fd4-65a5-4ad9-b1ee-180ee9559cf4
|
||||||
"identifiers": {
|
model: authentik_stages_prompt.prompt
|
||||||
"pk": "cb954fd4-65a5-4ad9-b1ee-180ee9559cf4"
|
attrs:
|
||||||
},
|
field_key: username
|
||||||
"model": "authentik_stages_prompt.prompt",
|
label: Username
|
||||||
"attrs": {
|
type: username
|
||||||
"field_key": "username",
|
required: true
|
||||||
"label": "Username",
|
placeholder: Username
|
||||||
"type": "username",
|
order: 0
|
||||||
"required": true,
|
"""
|
||||||
"placeholder": "Username",
|
|
||||||
"order": 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}"""
|
|
||||||
|
|
||||||
|
|
||||||
class TestFlowTransport(TransactionTestCase):
|
class TestBlueprintsV1(TransactionTestCase):
|
||||||
"""Test flow Transport"""
|
"""Test Blueprints"""
|
||||||
|
|
||||||
def test_bundle_invalid_format(self):
|
def test_blueprint_invalid_format(self):
|
||||||
"""Test bundle with invalid format"""
|
"""Test blueprint with invalid format"""
|
||||||
importer = Importer('{"version": 3}')
|
importer = Importer('{"version": 3}')
|
||||||
self.assertFalse(importer.validate()[0])
|
self.assertFalse(importer.validate()[0])
|
||||||
importer = Importer(
|
importer = Importer(
|
|
@ -0,0 +1,140 @@
|
||||||
|
"""Test blueprints v1 tasks"""
|
||||||
|
from tempfile import NamedTemporaryFile, mkdtemp
|
||||||
|
|
||||||
|
from django.test import TransactionTestCase
|
||||||
|
from yaml import dump
|
||||||
|
|
||||||
|
from authentik.blueprints.models import BlueprintInstance, BlueprintInstanceStatus
|
||||||
|
from authentik.blueprints.v1.tasks import apply_blueprint, blueprints_discover, blueprints_find
|
||||||
|
from authentik.lib.config import CONFIG
|
||||||
|
from authentik.lib.generators import generate_id
|
||||||
|
|
||||||
|
TMP = mkdtemp("authentik-blueprints")
|
||||||
|
|
||||||
|
|
||||||
|
class TestBlueprintsV1Tasks(TransactionTestCase):
|
||||||
|
"""Test Blueprints v1 Tasks"""
|
||||||
|
|
||||||
|
@CONFIG.patch("blueprints_dir", TMP)
|
||||||
|
def test_invalid_file_syntax(self):
|
||||||
|
"""Test syntactically invalid file"""
|
||||||
|
with NamedTemporaryFile(suffix=".yaml", dir=TMP) as file:
|
||||||
|
file.write(b"{")
|
||||||
|
file.flush()
|
||||||
|
blueprints = blueprints_find()
|
||||||
|
self.assertEqual(blueprints, [])
|
||||||
|
|
||||||
|
@CONFIG.patch("blueprints_dir", TMP)
|
||||||
|
def test_invalid_file_version(self):
|
||||||
|
"""Test invalid file"""
|
||||||
|
with NamedTemporaryFile(suffix=".yaml", dir=TMP) as file:
|
||||||
|
file.write(b"version: 2")
|
||||||
|
file.flush()
|
||||||
|
blueprints = blueprints_find()
|
||||||
|
self.assertEqual(blueprints, [])
|
||||||
|
|
||||||
|
@CONFIG.patch("blueprints_dir", TMP)
|
||||||
|
def test_valid(self):
|
||||||
|
"""Test valid file"""
|
||||||
|
with NamedTemporaryFile(mode="w+", suffix=".yaml", dir=TMP) as file:
|
||||||
|
file.write(
|
||||||
|
dump(
|
||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"entries": [],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
file.flush()
|
||||||
|
blueprints_discover() # pylint: disable=no-value-for-parameter
|
||||||
|
self.assertEqual(
|
||||||
|
BlueprintInstance.objects.first().last_applied_hash,
|
||||||
|
(
|
||||||
|
"e52bb445b03cd36057258dc9f0ce0fbed8278498ee1470e45315293e5f026d1b"
|
||||||
|
"d1f9b3526871c0003f5c07be5c3316d9d4a08444bd8fed1b3f03294e51e44522"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
self.assertEqual(BlueprintInstance.objects.first().metadata, {})
|
||||||
|
|
||||||
|
@CONFIG.patch("blueprints_dir", TMP)
|
||||||
|
def test_valid_updated(self):
|
||||||
|
"""Test valid file"""
|
||||||
|
with NamedTemporaryFile(mode="w+", suffix=".yaml", dir=TMP) as file:
|
||||||
|
file.write(
|
||||||
|
dump(
|
||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"entries": [],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
file.flush()
|
||||||
|
blueprints_discover() # pylint: disable=no-value-for-parameter
|
||||||
|
self.assertEqual(
|
||||||
|
BlueprintInstance.objects.first().last_applied_hash,
|
||||||
|
(
|
||||||
|
"e52bb445b03cd36057258dc9f0ce0fbed8278498ee1470e45315293e5f026d1b"
|
||||||
|
"d1f9b3526871c0003f5c07be5c3316d9d4a08444bd8fed1b3f03294e51e44522"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
self.assertEqual(BlueprintInstance.objects.first().metadata, {})
|
||||||
|
file.write(
|
||||||
|
dump(
|
||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"entries": [],
|
||||||
|
"metadata": {
|
||||||
|
"name": "foo",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
file.flush()
|
||||||
|
blueprints_discover() # pylint: disable=no-value-for-parameter
|
||||||
|
self.assertEqual(
|
||||||
|
BlueprintInstance.objects.first().last_applied_hash,
|
||||||
|
(
|
||||||
|
"fc62fea96067da8592bdf90927246d0ca150b045447df93b0652a0e20a8bc327"
|
||||||
|
"681510b5db37ea98759c61f9a98dd2381f46a3b5a2da69dfb45158897f14e824"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
BlueprintInstance.objects.first().metadata,
|
||||||
|
{
|
||||||
|
"name": "foo",
|
||||||
|
"labels": {},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
@CONFIG.patch("blueprints_dir", TMP)
|
||||||
|
def test_valid_disabled(self):
|
||||||
|
"""Test valid file"""
|
||||||
|
with NamedTemporaryFile(mode="w+", suffix=".yaml", dir=TMP) as file:
|
||||||
|
file.write(
|
||||||
|
dump(
|
||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"entries": [],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
file.flush()
|
||||||
|
instance: BlueprintInstance = BlueprintInstance.objects.create(
|
||||||
|
name=generate_id(),
|
||||||
|
path=file.name,
|
||||||
|
enabled=False,
|
||||||
|
status=BlueprintInstanceStatus.UNKNOWN,
|
||||||
|
)
|
||||||
|
instance.refresh_from_db()
|
||||||
|
self.assertEqual(instance.last_applied_hash, "")
|
||||||
|
self.assertEqual(
|
||||||
|
instance.status,
|
||||||
|
BlueprintInstanceStatus.UNKNOWN,
|
||||||
|
)
|
||||||
|
apply_blueprint(instance.pk) # pylint: disable=no-value-for-parameter
|
||||||
|
instance.refresh_from_db()
|
||||||
|
self.assertEqual(instance.last_applied_hash, "")
|
||||||
|
self.assertEqual(
|
||||||
|
instance.status,
|
||||||
|
BlueprintInstanceStatus.UNKNOWN,
|
||||||
|
)
|
|
@ -13,6 +13,7 @@ from yaml import SafeDumper, SafeLoader, ScalarNode, SequenceNode
|
||||||
|
|
||||||
from authentik.lib.models import SerializerModel
|
from authentik.lib.models import SerializerModel
|
||||||
from authentik.lib.sentry import SentryIgnoredException
|
from authentik.lib.sentry import SentryIgnoredException
|
||||||
|
from authentik.policies.models import PolicyBindingModel
|
||||||
|
|
||||||
|
|
||||||
def get_attrs(obj: SerializerModel) -> dict[str, Any]:
|
def get_attrs(obj: SerializerModel) -> dict[str, Any]:
|
||||||
|
@ -83,6 +84,14 @@ class BlueprintEntry:
|
||||||
return self.tag_resolver(self.identifiers, blueprint)
|
return self.tag_resolver(self.identifiers, blueprint)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class BlueprintMetadata:
|
||||||
|
"""Optional blueprint metadata"""
|
||||||
|
|
||||||
|
name: str
|
||||||
|
labels: dict[str, str] = field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Blueprint:
|
class Blueprint:
|
||||||
"""Dataclass used for a full export"""
|
"""Dataclass used for a full export"""
|
||||||
|
@ -90,6 +99,8 @@ class Blueprint:
|
||||||
version: int = field(default=1)
|
version: int = field(default=1)
|
||||||
entries: list[BlueprintEntry] = field(default_factory=list)
|
entries: list[BlueprintEntry] = field(default_factory=list)
|
||||||
|
|
||||||
|
metadata: Optional[BlueprintMetadata] = field(default=None)
|
||||||
|
|
||||||
|
|
||||||
class YAMLTag:
|
class YAMLTag:
|
||||||
"""Base class for all YAML Tags"""
|
"""Base class for all YAML Tags"""
|
||||||
|
@ -112,6 +123,13 @@ class KeyOf(YAMLTag):
|
||||||
def resolve(self, entry: BlueprintEntry, blueprint: Blueprint) -> Any:
|
def resolve(self, entry: BlueprintEntry, blueprint: Blueprint) -> Any:
|
||||||
for _entry in blueprint.entries:
|
for _entry in blueprint.entries:
|
||||||
if _entry.id == self.id_from and _entry._instance:
|
if _entry.id == self.id_from and _entry._instance:
|
||||||
|
# Special handling for PolicyBindingModels, as they'll have a different PK
|
||||||
|
# which is used when creating policy bindings
|
||||||
|
if (
|
||||||
|
isinstance(_entry._instance, PolicyBindingModel)
|
||||||
|
and entry.model.lower() == "authentik_policies.policybinding"
|
||||||
|
):
|
||||||
|
return _entry._instance.pbm_uuid
|
||||||
return _entry._instance.pk
|
return _entry._instance.pk
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"KeyOf: failed to find entry with `id` of `{self.id_from}` and a model instance"
|
f"KeyOf: failed to find entry with `id` of `{self.id_from}` and a model instance"
|
||||||
|
|
|
@ -74,6 +74,11 @@ class Importer:
|
||||||
except DaciteError as exc:
|
except DaciteError as exc:
|
||||||
raise EntryInvalidError from exc
|
raise EntryInvalidError from exc
|
||||||
|
|
||||||
|
@property
|
||||||
|
def blueprint(self) -> Blueprint:
|
||||||
|
"""Get imported blueprint"""
|
||||||
|
return self.__import
|
||||||
|
|
||||||
def __update_pks_for_attrs(self, attrs: dict[str, Any]) -> dict[str, Any]:
|
def __update_pks_for_attrs(self, attrs: dict[str, Any]) -> dict[str, Any]:
|
||||||
"""Replace any value if it is a known primary key of an other object"""
|
"""Replace any value if it is a known primary key of an other object"""
|
||||||
|
|
||||||
|
@ -190,7 +195,7 @@ class Importer:
|
||||||
try:
|
try:
|
||||||
serializer = self._validate_single(entry)
|
serializer = self._validate_single(entry)
|
||||||
except EntryInvalidError as exc:
|
except EntryInvalidError as exc:
|
||||||
self.logger.warning("entry not valid", entry=entry, error=exc)
|
self.logger.warning("entry invalid", entry=entry, error=exc)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
model = serializer.save()
|
model = serializer.save()
|
||||||
|
@ -215,5 +220,7 @@ class Importer:
|
||||||
successful = self._apply_models()
|
successful = self._apply_models()
|
||||||
if not successful:
|
if not successful:
|
||||||
self.logger.debug("blueprint validation failed")
|
self.logger.debug("blueprint validation failed")
|
||||||
|
for log in logs:
|
||||||
|
self.logger.debug(**log)
|
||||||
self.__import = orig_import
|
self.__import = orig_import
|
||||||
return successful, logs
|
return successful, logs
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
"""Blueprint labels"""
|
||||||
|
|
||||||
|
LABEL_AUTHENTIK_SYSTEM = "blueprints.goauthentik.io/system"
|
||||||
|
LABEL_AUTHENTIK_EXAMPLE = "blueprints.goauthentik.io/example"
|
|
@ -1,14 +1,21 @@
|
||||||
"""v1 blueprints tasks"""
|
"""v1 blueprints tasks"""
|
||||||
|
from dataclasses import asdict, dataclass, field
|
||||||
from glob import glob
|
from glob import glob
|
||||||
from hashlib import sha512
|
from hashlib import sha512
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from dacite import from_dict
|
||||||
from django.db import DatabaseError, InternalError, ProgrammingError
|
from django.db import DatabaseError, InternalError, ProgrammingError
|
||||||
|
from django.utils.timezone import now
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
from yaml import load
|
from yaml import load
|
||||||
|
from yaml.error import YAMLError
|
||||||
|
|
||||||
from authentik.blueprints.models import BlueprintInstance, BlueprintInstanceStatus
|
from authentik.blueprints.models import BlueprintInstance, BlueprintInstanceStatus
|
||||||
from authentik.blueprints.v1.common import BlueprintLoader
|
from authentik.blueprints.v1.common import BlueprintLoader, BlueprintMetadata
|
||||||
from authentik.blueprints.v1.importer import Importer
|
from authentik.blueprints.v1.importer import Importer
|
||||||
|
from authentik.blueprints.v1.labels import LABEL_AUTHENTIK_EXAMPLE
|
||||||
from authentik.events.monitored_tasks import (
|
from authentik.events.monitored_tasks import (
|
||||||
MonitoredTask,
|
MonitoredTask,
|
||||||
TaskResult,
|
TaskResult,
|
||||||
|
@ -19,41 +26,85 @@ from authentik.lib.config import CONFIG
|
||||||
from authentik.root.celery import CELERY_APP
|
from authentik.root.celery import CELERY_APP
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class BlueprintFile:
|
||||||
|
"""Basic info about a blueprint file"""
|
||||||
|
|
||||||
|
path: str
|
||||||
|
version: int
|
||||||
|
hash: str
|
||||||
|
last_m: int
|
||||||
|
meta: Optional[BlueprintMetadata] = field(default=None)
|
||||||
|
|
||||||
|
|
||||||
@CELERY_APP.task(
|
@CELERY_APP.task(
|
||||||
throws=(DatabaseError, ProgrammingError, InternalError),
|
throws=(DatabaseError, ProgrammingError, InternalError),
|
||||||
)
|
)
|
||||||
|
def blueprints_find():
|
||||||
|
"""Find blueprints and return valid ones"""
|
||||||
|
blueprints = []
|
||||||
|
for file in glob(f"{CONFIG.y('blueprints_dir')}/**/*.yaml", recursive=True):
|
||||||
|
path = Path(file)
|
||||||
|
with open(path, "r", encoding="utf-8") as blueprint_file:
|
||||||
|
try:
|
||||||
|
raw_blueprint = load(blueprint_file.read(), BlueprintLoader)
|
||||||
|
except YAMLError:
|
||||||
|
raw_blueprint = None
|
||||||
|
if not raw_blueprint:
|
||||||
|
continue
|
||||||
|
metadata = raw_blueprint.get("metadata", None)
|
||||||
|
version = raw_blueprint.get("version", 1)
|
||||||
|
if version != 1:
|
||||||
|
continue
|
||||||
|
file_hash = sha512(path.read_bytes()).hexdigest()
|
||||||
|
blueprint = BlueprintFile(str(path), version, file_hash, path.stat().st_mtime)
|
||||||
|
blueprint.meta = from_dict(BlueprintMetadata, metadata) if metadata else None
|
||||||
|
if (
|
||||||
|
blueprint.meta
|
||||||
|
and blueprint.meta.labels.get(LABEL_AUTHENTIK_EXAMPLE, "").lower() == "true"
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
blueprints.append(blueprint)
|
||||||
|
return blueprints
|
||||||
|
|
||||||
|
|
||||||
|
@CELERY_APP.task(
|
||||||
|
throws=(DatabaseError, ProgrammingError, InternalError), base=MonitoredTask, bind=True
|
||||||
|
)
|
||||||
@prefill_task
|
@prefill_task
|
||||||
def blueprints_discover():
|
def blueprints_discover(self: MonitoredTask):
|
||||||
"""Find blueprints and check if they need to be created in the database"""
|
"""Find blueprints and check if they need to be created in the database"""
|
||||||
for folder in CONFIG.y("blueprint_locations"):
|
count = 0
|
||||||
for file in glob(f"{folder}/**/*.yaml", recursive=True):
|
for blueprint in blueprints_find():
|
||||||
check_blueprint_v1_file(Path(file))
|
check_blueprint_v1_file(blueprint)
|
||||||
|
count += 1
|
||||||
|
self.set_status(
|
||||||
|
TaskResult(
|
||||||
|
TaskResultStatus.SUCCESSFUL,
|
||||||
|
messages=[_("Successfully imported %(count)d files." % {"count": count})],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def check_blueprint_v1_file(path: Path):
|
def check_blueprint_v1_file(blueprint: BlueprintFile):
|
||||||
"""Check if blueprint should be imported"""
|
"""Check if blueprint should be imported"""
|
||||||
with open(path, "r", encoding="utf-8") as blueprint_file:
|
rel_path = Path(blueprint.path).relative_to(Path(CONFIG.y("blueprints_dir")))
|
||||||
raw_blueprint = load(blueprint_file.read(), BlueprintLoader)
|
instance: BlueprintInstance = BlueprintInstance.objects.filter(path=blueprint.path).first()
|
||||||
version = raw_blueprint.get("version", 1)
|
|
||||||
if version != 1:
|
|
||||||
return
|
|
||||||
blueprint_file.seek(0)
|
|
||||||
file_hash = sha512(path.read_bytes()).hexdigest()
|
|
||||||
instance: BlueprintInstance = BlueprintInstance.objects.filter(path=path).first()
|
|
||||||
if not instance:
|
if not instance:
|
||||||
instance = BlueprintInstance(
|
instance = BlueprintInstance(
|
||||||
name=path.name,
|
name=blueprint.meta.name if blueprint.meta else str(rel_path),
|
||||||
path=str(path),
|
path=blueprint.path,
|
||||||
context={},
|
context={},
|
||||||
status=BlueprintInstanceStatus.UNKNOWN,
|
status=BlueprintInstanceStatus.UNKNOWN,
|
||||||
enabled=True,
|
enabled=True,
|
||||||
managed_models=[],
|
managed_models=[],
|
||||||
|
metadata={},
|
||||||
)
|
)
|
||||||
instance.save()
|
instance.save()
|
||||||
if instance.last_applied_hash != file_hash:
|
if instance.last_applied_hash != blueprint.hash:
|
||||||
apply_blueprint.delay(instance.pk.hex)
|
instance.metadata = asdict(blueprint.meta) if blueprint.meta else {}
|
||||||
instance.last_applied_hash = file_hash
|
|
||||||
instance.save()
|
instance.save()
|
||||||
|
apply_blueprint.delay(instance.pk.hex)
|
||||||
|
|
||||||
|
|
||||||
@CELERY_APP.task(
|
@CELERY_APP.task(
|
||||||
|
@ -62,25 +113,33 @@ def check_blueprint_v1_file(path: Path):
|
||||||
)
|
)
|
||||||
def apply_blueprint(self: MonitoredTask, instance_pk: str):
|
def apply_blueprint(self: MonitoredTask, instance_pk: str):
|
||||||
"""Apply single blueprint"""
|
"""Apply single blueprint"""
|
||||||
|
self.set_uid(instance_pk)
|
||||||
self.save_on_success = False
|
self.save_on_success = False
|
||||||
try:
|
try:
|
||||||
instance: BlueprintInstance = BlueprintInstance.objects.filter(pk=instance_pk).first()
|
instance: BlueprintInstance = BlueprintInstance.objects.filter(pk=instance_pk).first()
|
||||||
if not instance or not instance.enabled:
|
if not instance or not instance.enabled:
|
||||||
return
|
return
|
||||||
|
file_hash = sha512(Path(instance.path).read_bytes()).hexdigest()
|
||||||
with open(instance.path, "r", encoding="utf-8") as blueprint_file:
|
with open(instance.path, "r", encoding="utf-8") as blueprint_file:
|
||||||
importer = Importer(blueprint_file.read())
|
importer = Importer(blueprint_file.read())
|
||||||
valid, logs = importer.validate()
|
valid, logs = importer.validate()
|
||||||
if not valid:
|
if not valid:
|
||||||
instance.status = BlueprintInstanceStatus.ERROR
|
instance.status = BlueprintInstanceStatus.ERROR
|
||||||
instance.save()
|
instance.save()
|
||||||
self.set_status(TaskResult(TaskResultStatus.ERROR, [x["event"] for x in logs]))
|
self.set_status(TaskResult(TaskResultStatus.ERROR, [x["event"] for x in logs]))
|
||||||
return
|
return
|
||||||
applied = importer.apply()
|
applied = importer.apply()
|
||||||
if not applied:
|
if not applied:
|
||||||
instance.status = BlueprintInstanceStatus.ERROR
|
instance.status = BlueprintInstanceStatus.ERROR
|
||||||
instance.save()
|
instance.save()
|
||||||
self.set_status(TaskResult(TaskResultStatus.ERROR, "Failed to apply"))
|
self.set_status(TaskResult(TaskResultStatus.ERROR, "Failed to apply"))
|
||||||
except (DatabaseError, ProgrammingError, InternalError) as exc:
|
return
|
||||||
|
instance.status = BlueprintInstanceStatus.SUCCESSFUL
|
||||||
|
instance.last_applied_hash = file_hash
|
||||||
|
instance.last_applied = now()
|
||||||
|
instance.save()
|
||||||
|
self.set_status(TaskResult(TaskResultStatus.SUCCESSFUL))
|
||||||
|
except (DatabaseError, ProgrammingError, InternalError, IOError) as exc:
|
||||||
instance.status = BlueprintInstanceStatus.ERROR
|
instance.status = BlueprintInstanceStatus.ERROR
|
||||||
instance.save()
|
instance.save()
|
||||||
self.set_status(TaskResult(TaskResultStatus.ERROR).with_error(exc))
|
self.set_status(TaskResult(TaskResultStatus.ERROR).with_error(exc))
|
||||||
|
|
|
@ -4,7 +4,7 @@ from django.core.management.base import BaseCommand
|
||||||
from authentik.root.celery import _get_startup_tasks
|
from authentik.root.celery import _get_startup_tasks
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand): # pragma: no cover
|
class Command(BaseCommand):
|
||||||
"""Run bootstrap tasks to ensure certain objects are created"""
|
"""Run bootstrap tasks to ensure certain objects are created"""
|
||||||
|
|
||||||
def handle(self, **options):
|
def handle(self, **options):
|
||||||
|
|
|
@ -5,7 +5,7 @@ from django.core.management.base import BaseCommand, no_translations
|
||||||
from guardian.management import create_anonymous_user
|
from guardian.management import create_anonymous_user
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand): # pragma: no cover
|
class Command(BaseCommand):
|
||||||
"""Repair missing permissions"""
|
"""Repair missing permissions"""
|
||||||
|
|
||||||
@no_translations
|
@no_translations
|
||||||
|
|
|
@ -22,7 +22,7 @@ BANNER_TEXT = """### authentik shell ({authentik})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand): # pragma: no cover
|
class Command(BaseCommand):
|
||||||
"""Start the Django shell with all authentik models already imported"""
|
"""Start the Django shell with all authentik models already imported"""
|
||||||
|
|
||||||
django_models = {}
|
django_models = {}
|
||||||
|
|
|
@ -3,6 +3,7 @@ from datetime import datetime
|
||||||
from typing import TYPE_CHECKING, Optional
|
from typing import TYPE_CHECKING, Optional
|
||||||
|
|
||||||
from authentik.blueprints.manager import ManagedAppConfig
|
from authentik.blueprints.manager import ManagedAppConfig
|
||||||
|
from authentik.lib.generators import generate_id
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from authentik.crypto.models import CertificateKeyPair
|
from authentik.crypto.models import CertificateKeyPair
|
||||||
|
@ -53,3 +54,19 @@ class AuthentikCryptoConfig(ManagedAppConfig):
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
if now < cert.certificate.not_valid_before or now > cert.certificate.not_valid_after:
|
if now < cert.certificate.not_valid_before or now > cert.certificate.not_valid_after:
|
||||||
self._create_update_cert(cert)
|
self._create_update_cert(cert)
|
||||||
|
|
||||||
|
def reconcile_self_signed(self):
|
||||||
|
"""Create self-signed keypair"""
|
||||||
|
from authentik.crypto.builder import CertificateBuilder
|
||||||
|
from authentik.crypto.models import CertificateKeyPair
|
||||||
|
|
||||||
|
name = "authentik Self-signed Certificate"
|
||||||
|
if CertificateKeyPair.objects.filter(name=name).exists():
|
||||||
|
return
|
||||||
|
builder = CertificateBuilder()
|
||||||
|
builder.build(subject_alt_names=[f"{generate_id()}.self-signed.goauthentik.io"])
|
||||||
|
CertificateKeyPair.objects.create(
|
||||||
|
name="authentik Self-signed Certificate",
|
||||||
|
certificate_data=builder.certificate,
|
||||||
|
key_data=builder.private_key,
|
||||||
|
)
|
||||||
|
|
|
@ -5,24 +5,10 @@ from django.db import migrations
|
||||||
from authentik.lib.generators import generate_id
|
from authentik.lib.generators import generate_id
|
||||||
|
|
||||||
|
|
||||||
def create_self_signed(apps, schema_editor):
|
|
||||||
CertificateKeyPair = apps.get_model("authentik_crypto", "CertificateKeyPair")
|
|
||||||
db_alias = schema_editor.connection.alias
|
|
||||||
from authentik.crypto.builder import CertificateBuilder
|
|
||||||
|
|
||||||
builder = CertificateBuilder()
|
|
||||||
builder.build(subject_alt_names=[f"{generate_id()}.self-signed.goauthentik.io"])
|
|
||||||
CertificateKeyPair.objects.using(db_alias).create(
|
|
||||||
name="authentik Self-signed Certificate",
|
|
||||||
certificate_data=builder.certificate,
|
|
||||||
key_data=builder.private_key,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("authentik_crypto", "0001_initial"),
|
("authentik_crypto", "0001_initial"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [migrations.RunPython(create_self_signed)]
|
operations = []
|
||||||
|
|
|
@ -48,7 +48,7 @@ class FlowPlanProcess(PROCESS_CLASS): # pragma: no cover
|
||||||
self.return_dict[self.index] = diffs
|
self.return_dict[self.index] = diffs
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand): # pragma: no cover
|
class Command(BaseCommand):
|
||||||
"""Benchmark authentik"""
|
"""Benchmark authentik"""
|
||||||
|
|
||||||
def add_arguments(self, parser):
|
def add_arguments(self, parser):
|
||||||
|
|
|
@ -79,5 +79,4 @@ cert_discovery_dir: /certs
|
||||||
default_token_length: 128
|
default_token_length: 128
|
||||||
impersonation: true
|
impersonation: true
|
||||||
|
|
||||||
blueprint_locations:
|
blueprints_dir: /blueprints
|
||||||
- /blueprints
|
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
"""Docker controller tests"""
|
"""Docker controller tests"""
|
||||||
from django.apps import apps
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from docker.models.containers import Container
|
from docker.models.containers import Container
|
||||||
|
|
||||||
|
from authentik.blueprints.tests import reconcile_app
|
||||||
from authentik.outposts.apps import MANAGED_OUTPOST
|
from authentik.outposts.apps import MANAGED_OUTPOST
|
||||||
from authentik.outposts.controllers.base import ControllerException
|
from authentik.outposts.controllers.base import ControllerException
|
||||||
from authentik.outposts.controllers.docker import DockerController
|
from authentik.outposts.controllers.docker import DockerController
|
||||||
|
@ -13,13 +13,13 @@ from authentik.providers.proxy.controllers.docker import ProxyDockerController
|
||||||
class DockerControllerTests(TestCase):
|
class DockerControllerTests(TestCase):
|
||||||
"""Docker controller tests"""
|
"""Docker controller tests"""
|
||||||
|
|
||||||
|
@reconcile_app("authentik_outposts")
|
||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
self.outpost = Outpost.objects.create(
|
self.outpost = Outpost.objects.create(
|
||||||
name="test",
|
name="test",
|
||||||
type=OutpostType.PROXY,
|
type=OutpostType.PROXY,
|
||||||
)
|
)
|
||||||
self.integration = DockerServiceConnection(name="test")
|
self.integration = DockerServiceConnection(name="test")
|
||||||
apps.get_app_config("authentik_outposts").reconcile()
|
|
||||||
|
|
||||||
def test_init_managed(self):
|
def test_init_managed(self):
|
||||||
"""Docker controller shouldn't do anything for managed outpost"""
|
"""Docker controller shouldn't do anything for managed outpost"""
|
||||||
|
|
|
@ -5,7 +5,7 @@ from django.test import RequestFactory
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from jwt import decode
|
from jwt import decode
|
||||||
|
|
||||||
from authentik.blueprints import apply_blueprint
|
from authentik.blueprints.tests import apply_blueprint
|
||||||
from authentik.core.models import USER_ATTRIBUTE_SA, Application, Group, Token, TokenIntents
|
from authentik.core.models import USER_ATTRIBUTE_SA, Application, Group, Token, TokenIntents
|
||||||
from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow
|
from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow
|
||||||
from authentik.lib.generators import generate_id, generate_key
|
from authentik.lib.generators import generate_id, generate_key
|
||||||
|
|
|
@ -6,7 +6,7 @@ from django.test import RequestFactory
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from jwt import decode
|
from jwt import decode
|
||||||
|
|
||||||
from authentik.blueprints import apply_blueprint
|
from authentik.blueprints.tests import apply_blueprint
|
||||||
from authentik.core.models import Application, Group
|
from authentik.core.models import Application, Group
|
||||||
from authentik.core.tests.utils import create_test_cert, create_test_flow
|
from authentik.core.tests.utils import create_test_cert, create_test_flow
|
||||||
from authentik.lib.generators import generate_id, generate_key
|
from authentik.lib.generators import generate_id, generate_key
|
||||||
|
|
|
@ -4,7 +4,7 @@ from dataclasses import asdict
|
||||||
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from authentik.blueprints import apply_blueprint
|
from authentik.blueprints.tests import apply_blueprint
|
||||||
from authentik.core.models import Application
|
from authentik.core.models import Application
|
||||||
from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow
|
from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow
|
||||||
from authentik.events.models import Event, EventAction
|
from authentik.events.models import Event, EventAction
|
||||||
|
|
|
@ -4,7 +4,7 @@ from base64 import b64encode
|
||||||
from django.http.request import QueryDict
|
from django.http.request import QueryDict
|
||||||
from django.test import RequestFactory, TestCase
|
from django.test import RequestFactory, TestCase
|
||||||
|
|
||||||
from authentik.blueprints import apply_blueprint
|
from authentik.blueprints.tests import apply_blueprint
|
||||||
from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow
|
from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow
|
||||||
from authentik.crypto.models import CertificateKeyPair
|
from authentik.crypto.models import CertificateKeyPair
|
||||||
from authentik.events.models import Event, EventAction
|
from authentik.events.models import Event, EventAction
|
||||||
|
|
|
@ -4,7 +4,7 @@ from base64 import b64encode
|
||||||
from django.test import RequestFactory, TestCase
|
from django.test import RequestFactory, TestCase
|
||||||
from lxml import etree # nosec
|
from lxml import etree # nosec
|
||||||
|
|
||||||
from authentik.blueprints import apply_blueprint
|
from authentik.blueprints.tests import apply_blueprint
|
||||||
from authentik.core.tests.utils import create_test_cert, create_test_flow
|
from authentik.core.tests.utils import create_test_cert, create_test_flow
|
||||||
from authentik.lib.tests.utils import get_request
|
from authentik.lib.tests.utils import get_request
|
||||||
from authentik.lib.xml import lxml_from_string
|
from authentik.lib.xml import lxml_from_string
|
||||||
|
|
|
@ -4,7 +4,7 @@ from unittest.mock import Mock, PropertyMock, patch
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from authentik.blueprints import apply_blueprint
|
from authentik.blueprints.tests import apply_blueprint
|
||||||
from authentik.core.models import User
|
from authentik.core.models import User
|
||||||
from authentik.lib.generators import generate_key
|
from authentik.lib.generators import generate_key
|
||||||
from authentik.sources.ldap.auth import LDAPBackend
|
from authentik.sources.ldap.auth import LDAPBackend
|
||||||
|
|
|
@ -4,7 +4,7 @@ from unittest.mock import PropertyMock, patch
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from authentik.blueprints import apply_blueprint
|
from authentik.blueprints.tests import apply_blueprint
|
||||||
from authentik.core.models import Group, User
|
from authentik.core.models import Group, User
|
||||||
from authentik.core.tests.utils import create_test_admin_user
|
from authentik.core.tests.utils import create_test_admin_user
|
||||||
from authentik.events.models import Event, EventAction
|
from authentik.events.models import Event, EventAction
|
||||||
|
|
|
@ -8,7 +8,7 @@ from authentik.stages.email.tasks import send_mail
|
||||||
from authentik.stages.email.utils import TemplateEmailMessage
|
from authentik.stages.email.utils import TemplateEmailMessage
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand): # pragma: no cover
|
class Command(BaseCommand):
|
||||||
"""Send a test-email with global settings"""
|
"""Send a test-email with global settings"""
|
||||||
|
|
||||||
@no_translations
|
@no_translations
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
metadata:
|
||||||
|
name: Default - Password change flow
|
||||||
entries:
|
entries:
|
||||||
- attrs:
|
- attrs:
|
||||||
compatibility_mode: false
|
compatibility_mode: false
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
metadata:
|
||||||
|
name: Default - Authentication flow
|
||||||
entries:
|
entries:
|
||||||
- attrs:
|
- attrs:
|
||||||
cache_count: 1
|
cache_count: 1
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
metadata:
|
||||||
|
name: Default - Invalidation flow
|
||||||
entries:
|
entries:
|
||||||
- attrs:
|
- attrs:
|
||||||
compatibility_mode: false
|
compatibility_mode: false
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
metadata:
|
||||||
|
name: Default - Static MFA setup flow
|
||||||
entries:
|
entries:
|
||||||
- attrs:
|
- attrs:
|
||||||
compatibility_mode: false
|
compatibility_mode: false
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
metadata:
|
||||||
|
name: Default - TOTP MFA setup flow
|
||||||
entries:
|
entries:
|
||||||
- attrs:
|
- attrs:
|
||||||
compatibility_mode: false
|
compatibility_mode: false
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
metadata:
|
||||||
|
name: Default - WebAuthn MFA setup flow
|
||||||
entries:
|
entries:
|
||||||
- attrs:
|
- attrs:
|
||||||
compatibility_mode: false
|
compatibility_mode: false
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
metadata:
|
||||||
|
name: Default - Provider authorization flow (explicit consent)
|
||||||
entries:
|
entries:
|
||||||
- attrs:
|
- attrs:
|
||||||
compatibility_mode: false
|
compatibility_mode: false
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
metadata:
|
||||||
|
name: Default - Provider authorization flow (implicit consent)
|
||||||
entries:
|
entries:
|
||||||
- attrs:
|
- attrs:
|
||||||
compatibility_mode: false
|
compatibility_mode: false
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
metadata:
|
||||||
|
name: Default - Source authentication flow
|
||||||
entries:
|
entries:
|
||||||
- attrs:
|
- attrs:
|
||||||
compatibility_mode: false
|
compatibility_mode: false
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
metadata:
|
||||||
|
name: Default - Source enrollment flow
|
||||||
entries:
|
entries:
|
||||||
- attrs:
|
- attrs:
|
||||||
compatibility_mode: false
|
compatibility_mode: false
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
metadata:
|
||||||
|
name: Default - Source pre-authentication flow
|
||||||
entries:
|
entries:
|
||||||
- attrs:
|
- attrs:
|
||||||
compatibility_mode: false
|
compatibility_mode: false
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
metadata:
|
||||||
|
name: Default - User settings flow
|
||||||
entries:
|
entries:
|
||||||
- attrs:
|
- attrs:
|
||||||
compatibility_mode: false
|
compatibility_mode: false
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
metadata:
|
||||||
|
name: Default - Tenant
|
||||||
version: 1
|
version: 1
|
||||||
entries:
|
entries:
|
||||||
- attrs:
|
- attrs:
|
||||||
|
|
|
@ -1,4 +1,8 @@
|
||||||
version: 1
|
version: 1
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
blueprints.goauthentik.io/example: "true"
|
||||||
|
name: Example - Enrollment (2 Stage)
|
||||||
entries:
|
entries:
|
||||||
- identifiers:
|
- identifiers:
|
||||||
slug: default-enrollment-flow
|
slug: default-enrollment-flow
|
||||||
|
|
|
@ -1,4 +1,8 @@
|
||||||
version: 1
|
version: 1
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
blueprints.goauthentik.io/example: "true"
|
||||||
|
name: Example - Enrollment with email verification
|
||||||
entries:
|
entries:
|
||||||
- identifiers:
|
- identifiers:
|
||||||
slug: default-enrollment-flow
|
slug: default-enrollment-flow
|
||||||
|
|
|
@ -1,4 +1,8 @@
|
||||||
version: 1
|
version: 1
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
blueprints.goauthentik.io/example: "true"
|
||||||
|
name: Example - Two-factor Login
|
||||||
entries:
|
entries:
|
||||||
- identifiers:
|
- identifiers:
|
||||||
slug: default-authentication-flow
|
slug: default-authentication-flow
|
||||||
|
|
|
@ -1,4 +1,8 @@
|
||||||
version: 1
|
version: 1
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
blueprints.goauthentik.io/example: "true"
|
||||||
|
name: Example - Login with conditional Captcha
|
||||||
entries:
|
entries:
|
||||||
- identifiers:
|
- identifiers:
|
||||||
slug: default-authentication-flow
|
slug: default-authentication-flow
|
||||||
|
|
|
@ -1,4 +1,8 @@
|
||||||
version: 1
|
version: 1
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
blueprints.goauthentik.io/example: "true"
|
||||||
|
name: Example - Recovery with email verification
|
||||||
entries:
|
entries:
|
||||||
- identifiers:
|
- identifiers:
|
||||||
slug: default-recovery-flow
|
slug: default-recovery-flow
|
||||||
|
|
|
@ -1,4 +1,8 @@
|
||||||
version: 1
|
version: 1
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
blueprints.goauthentik.io/example: "true"
|
||||||
|
name: Example - User deletion
|
||||||
entries:
|
entries:
|
||||||
- identifiers:
|
- identifiers:
|
||||||
slug: default-unenrollment-flow
|
slug: default-unenrollment-flow
|
||||||
|
|
|
@ -1,4 +1,8 @@
|
||||||
version: 1
|
version: 1
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
blueprints.goauthentik.io/system: "true"
|
||||||
|
name: System - OAuth2 Provider - Scopes
|
||||||
entries:
|
entries:
|
||||||
- identifiers:
|
- identifiers:
|
||||||
managed: goauthentik.io/providers/oauth2/scope-openid
|
managed: goauthentik.io/providers/oauth2/scope-openid
|
||||||
|
|
|
@ -1,4 +1,8 @@
|
||||||
version: 1
|
version: 1
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
blueprints.goauthentik.io/system: "true"
|
||||||
|
name: System - Proxy Provider - Scopes
|
||||||
entries:
|
entries:
|
||||||
- identifiers:
|
- identifiers:
|
||||||
managed: goauthentik.io/providers/proxy/scope-proxy
|
managed: goauthentik.io/providers/proxy/scope-proxy
|
||||||
|
|
|
@ -1,4 +1,8 @@
|
||||||
version: 1
|
version: 1
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
blueprints.goauthentik.io/system: "true"
|
||||||
|
name: System - SAML Provider - Mappings
|
||||||
entries:
|
entries:
|
||||||
- identifiers:
|
- identifiers:
|
||||||
managed: goauthentik.io/providers/saml/upn
|
managed: goauthentik.io/providers/saml/upn
|
||||||
|
|
|
@ -1,4 +1,8 @@
|
||||||
version: 1
|
version: 1
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
blueprints.goauthentik.io/system: "true"
|
||||||
|
name: System - LDAP Source - Mappings
|
||||||
entries:
|
entries:
|
||||||
- identifiers:
|
- identifiers:
|
||||||
managed: goauthentik.io/sources/ldap/default-name
|
managed: goauthentik.io/sources/ldap/default-name
|
||||||
|
|
|
@ -29,7 +29,14 @@ force_to_top = "*"
|
||||||
[tool.coverage.run]
|
[tool.coverage.run]
|
||||||
source = ["authentik"]
|
source = ["authentik"]
|
||||||
relative_files = true
|
relative_files = true
|
||||||
omit = ["*/asgi.py", "manage.py", "*/migrations/*", "*/apps.py", "website/"]
|
omit = [
|
||||||
|
"*/asgi.py",
|
||||||
|
"manage.py",
|
||||||
|
"*/migrations/*",
|
||||||
|
"*/management/commands/*",
|
||||||
|
"*/apps.py",
|
||||||
|
"website/",
|
||||||
|
]
|
||||||
|
|
||||||
[tool.coverage.report]
|
[tool.coverage.report]
|
||||||
sort = "Cover"
|
sort = "Cover"
|
||||||
|
|
105
schema.yml
105
schema.yml
|
@ -6218,6 +6218,62 @@ paths:
|
||||||
$ref: '#/components/schemas/ValidationError'
|
$ref: '#/components/schemas/ValidationError'
|
||||||
'403':
|
'403':
|
||||||
$ref: '#/components/schemas/GenericError'
|
$ref: '#/components/schemas/GenericError'
|
||||||
|
/managed/blueprints/{instance_uuid}/apply/:
|
||||||
|
post:
|
||||||
|
operationId: managed_blueprints_apply_create
|
||||||
|
description: Apply a blueprint
|
||||||
|
parameters:
|
||||||
|
- in: path
|
||||||
|
name: instance_uuid
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
description: A UUID string identifying this Blueprint Instance.
|
||||||
|
required: true
|
||||||
|
tags:
|
||||||
|
- managed
|
||||||
|
security:
|
||||||
|
- authentik: []
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/BlueprintInstance'
|
||||||
|
description: ''
|
||||||
|
'400':
|
||||||
|
$ref: '#/components/schemas/ValidationError'
|
||||||
|
'403':
|
||||||
|
$ref: '#/components/schemas/GenericError'
|
||||||
|
/managed/blueprints/{instance_uuid}/used_by/:
|
||||||
|
get:
|
||||||
|
operationId: managed_blueprints_used_by_list
|
||||||
|
description: Get a list of all objects that use this object
|
||||||
|
parameters:
|
||||||
|
- in: path
|
||||||
|
name: instance_uuid
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
description: A UUID string identifying this Blueprint Instance.
|
||||||
|
required: true
|
||||||
|
tags:
|
||||||
|
- managed
|
||||||
|
security:
|
||||||
|
- authentik: []
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/UsedBy'
|
||||||
|
description: ''
|
||||||
|
'400':
|
||||||
|
$ref: '#/components/schemas/ValidationError'
|
||||||
|
'403':
|
||||||
|
$ref: '#/components/schemas/GenericError'
|
||||||
/managed/blueprints/available/:
|
/managed/blueprints/available/:
|
||||||
get:
|
get:
|
||||||
operationId: managed_blueprints_available_list
|
operationId: managed_blueprints_available_list
|
||||||
|
@ -6233,7 +6289,7 @@ paths:
|
||||||
schema:
|
schema:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
type: string
|
$ref: '#/components/schemas/BlueprintFile'
|
||||||
description: ''
|
description: ''
|
||||||
'400':
|
'400':
|
||||||
$ref: '#/components/schemas/ValidationError'
|
$ref: '#/components/schemas/ValidationError'
|
||||||
|
@ -20862,6 +20918,25 @@ components:
|
||||||
- POST
|
- POST
|
||||||
- POST_AUTO
|
- POST_AUTO
|
||||||
type: string
|
type: string
|
||||||
|
BlueprintFile:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
path:
|
||||||
|
type: string
|
||||||
|
last_m:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
hash:
|
||||||
|
type: string
|
||||||
|
meta:
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/components/schemas/Metadata'
|
||||||
|
readOnly: true
|
||||||
|
required:
|
||||||
|
- hash
|
||||||
|
- last_m
|
||||||
|
- meta
|
||||||
|
- path
|
||||||
BlueprintInstance:
|
BlueprintInstance:
|
||||||
type: object
|
type: object
|
||||||
description: Info about a single blueprint instance file
|
description: Info about a single blueprint instance file
|
||||||
|
@ -20886,7 +20961,9 @@ components:
|
||||||
type: string
|
type: string
|
||||||
readOnly: true
|
readOnly: true
|
||||||
status:
|
status:
|
||||||
$ref: '#/components/schemas/BlueprintInstanceStatusEnum'
|
allOf:
|
||||||
|
- $ref: '#/components/schemas/BlueprintInstanceStatusEnum'
|
||||||
|
readOnly: true
|
||||||
enabled:
|
enabled:
|
||||||
type: boolean
|
type: boolean
|
||||||
managed_models:
|
managed_models:
|
||||||
|
@ -20894,11 +20971,15 @@ components:
|
||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
readOnly: true
|
readOnly: true
|
||||||
|
metadata:
|
||||||
|
type: object
|
||||||
|
additionalProperties: {}
|
||||||
|
readOnly: true
|
||||||
required:
|
required:
|
||||||
- context
|
|
||||||
- last_applied
|
- last_applied
|
||||||
- last_applied_hash
|
- last_applied_hash
|
||||||
- managed_models
|
- managed_models
|
||||||
|
- metadata
|
||||||
- name
|
- name
|
||||||
- path
|
- path
|
||||||
- pk
|
- pk
|
||||||
|
@ -20916,15 +20997,11 @@ components:
|
||||||
context:
|
context:
|
||||||
type: object
|
type: object
|
||||||
additionalProperties: {}
|
additionalProperties: {}
|
||||||
status:
|
|
||||||
$ref: '#/components/schemas/BlueprintInstanceStatusEnum'
|
|
||||||
enabled:
|
enabled:
|
||||||
type: boolean
|
type: boolean
|
||||||
required:
|
required:
|
||||||
- context
|
|
||||||
- name
|
- name
|
||||||
- path
|
- path
|
||||||
- status
|
|
||||||
BlueprintInstanceStatusEnum:
|
BlueprintInstanceStatusEnum:
|
||||||
enum:
|
enum:
|
||||||
- successful
|
- successful
|
||||||
|
@ -23774,6 +23851,18 @@ components:
|
||||||
required:
|
required:
|
||||||
- challenge
|
- challenge
|
||||||
- name
|
- name
|
||||||
|
Metadata:
|
||||||
|
type: object
|
||||||
|
description: Serializer for blueprint metadata
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
labels:
|
||||||
|
type: object
|
||||||
|
additionalProperties: {}
|
||||||
|
required:
|
||||||
|
- labels
|
||||||
|
- name
|
||||||
NameIdPolicyEnum:
|
NameIdPolicyEnum:
|
||||||
enum:
|
enum:
|
||||||
- urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress
|
- urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress
|
||||||
|
@ -27808,8 +27897,6 @@ components:
|
||||||
context:
|
context:
|
||||||
type: object
|
type: object
|
||||||
additionalProperties: {}
|
additionalProperties: {}
|
||||||
status:
|
|
||||||
$ref: '#/components/schemas/BlueprintInstanceStatusEnum'
|
|
||||||
enabled:
|
enabled:
|
||||||
type: boolean
|
type: boolean
|
||||||
PatchedCaptchaStageRequest:
|
PatchedCaptchaStageRequest:
|
||||||
|
|
|
@ -13,8 +13,8 @@ with open("local.env.yml", "w") as _config:
|
||||||
},
|
},
|
||||||
"outposts": {
|
"outposts": {
|
||||||
"container_image_base": "ghcr.io/goauthentik/dev-%(type)s:gh-%(build_hash)s",
|
"container_image_base": "ghcr.io/goauthentik/dev-%(type)s:gh-%(build_hash)s",
|
||||||
"blueprint_locations": ["./blueprints"],
|
|
||||||
},
|
},
|
||||||
|
"blueprints_dir": "./blueprints",
|
||||||
"web": {
|
"web": {
|
||||||
"outpost_port_offset": 100,
|
"outpost_port_offset": 100,
|
||||||
},
|
},
|
||||||
|
|
|
@ -13,7 +13,7 @@ from selenium.webdriver.common.keys import Keys
|
||||||
from selenium.webdriver.support import expected_conditions as ec
|
from selenium.webdriver.support import expected_conditions as ec
|
||||||
from selenium.webdriver.support.wait import WebDriverWait
|
from selenium.webdriver.support.wait import WebDriverWait
|
||||||
|
|
||||||
from authentik.blueprints import apply_blueprint
|
from authentik.blueprints.tests import apply_blueprint
|
||||||
from authentik.flows.models import Flow
|
from authentik.flows.models import Flow
|
||||||
from authentik.stages.authenticator_static.models import AuthenticatorStaticStage
|
from authentik.stages.authenticator_static.models import AuthenticatorStaticStage
|
||||||
from authentik.stages.authenticator_totp.models import AuthenticatorTOTPStage
|
from authentik.stages.authenticator_totp.models import AuthenticatorTOTPStage
|
||||||
|
|
|
@ -9,7 +9,7 @@ from selenium.webdriver.common.by import By
|
||||||
from selenium.webdriver.support import expected_conditions as ec
|
from selenium.webdriver.support import expected_conditions as ec
|
||||||
from selenium.webdriver.support.wait import WebDriverWait
|
from selenium.webdriver.support.wait import WebDriverWait
|
||||||
|
|
||||||
from authentik.blueprints import apply_blueprint
|
from authentik.blueprints.tests import apply_blueprint
|
||||||
from authentik.core.models import User
|
from authentik.core.models import User
|
||||||
from authentik.core.tests.utils import create_test_flow
|
from authentik.core.tests.utils import create_test_flow
|
||||||
from authentik.flows.models import FlowDesignation, FlowStageBinding
|
from authentik.flows.models import FlowDesignation, FlowStageBinding
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
from sys import platform
|
from sys import platform
|
||||||
from unittest.case import skipUnless
|
from unittest.case import skipUnless
|
||||||
|
|
||||||
from authentik.blueprints import apply_blueprint
|
from authentik.blueprints.tests import apply_blueprint
|
||||||
from tests.e2e.utils import SeleniumTestCase, retry
|
from tests.e2e.utils import SeleniumTestCase, retry
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ from unittest.case import skipUnless
|
||||||
from selenium.webdriver.common.by import By
|
from selenium.webdriver.common.by import By
|
||||||
from selenium.webdriver.common.keys import Keys
|
from selenium.webdriver.common.keys import Keys
|
||||||
|
|
||||||
from authentik.blueprints import apply_blueprint
|
from authentik.blueprints.tests import apply_blueprint
|
||||||
from authentik.core.models import User
|
from authentik.core.models import User
|
||||||
from authentik.flows.models import Flow, FlowDesignation
|
from authentik.flows.models import Flow, FlowDesignation
|
||||||
from authentik.lib.generators import generate_key
|
from authentik.lib.generators import generate_key
|
||||||
|
|
|
@ -10,14 +10,14 @@ from guardian.shortcuts import get_anonymous_user
|
||||||
from ldap3 import ALL, ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES, SUBTREE, Connection, Server
|
from ldap3 import ALL, ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES, SUBTREE, Connection, Server
|
||||||
from ldap3.core.exceptions import LDAPInvalidCredentialsResult
|
from ldap3.core.exceptions import LDAPInvalidCredentialsResult
|
||||||
|
|
||||||
from authentik.blueprints import apply_blueprint
|
from authentik.blueprints.tests import apply_blueprint, reconcile_app
|
||||||
from authentik.core.models import Application, User
|
from authentik.core.models import Application, User
|
||||||
from authentik.events.models import Event, EventAction
|
from authentik.events.models import Event, EventAction
|
||||||
from authentik.flows.models import Flow
|
from authentik.flows.models import Flow
|
||||||
from authentik.outposts.apps import MANAGED_OUTPOST
|
from authentik.outposts.apps import MANAGED_OUTPOST
|
||||||
from authentik.outposts.models import Outpost, OutpostConfig, OutpostType
|
from authentik.outposts.models import Outpost, OutpostConfig, OutpostType
|
||||||
from authentik.providers.ldap.models import APIAccessMode, LDAPProvider
|
from authentik.providers.ldap.models import APIAccessMode, LDAPProvider
|
||||||
from tests.e2e.utils import SeleniumTestCase, reconcile_app, retry
|
from tests.e2e.utils import SeleniumTestCase, retry
|
||||||
|
|
||||||
|
|
||||||
@skipUnless(platform.startswith("linux"), "requires local docker")
|
@skipUnless(platform.startswith("linux"), "requires local docker")
|
||||||
|
|
|
@ -8,14 +8,14 @@ from docker.types import Healthcheck
|
||||||
from selenium.webdriver.common.by import By
|
from selenium.webdriver.common.by import By
|
||||||
from selenium.webdriver.support import expected_conditions as ec
|
from selenium.webdriver.support import expected_conditions as ec
|
||||||
|
|
||||||
from authentik.blueprints import apply_blueprint
|
from authentik.blueprints.tests import apply_blueprint, reconcile_app
|
||||||
from authentik.core.models import Application
|
from authentik.core.models import Application
|
||||||
from authentik.flows.models import Flow
|
from authentik.flows.models import Flow
|
||||||
from authentik.lib.generators import generate_id, generate_key
|
from authentik.lib.generators import generate_id, generate_key
|
||||||
from authentik.policies.expression.models import ExpressionPolicy
|
from authentik.policies.expression.models import ExpressionPolicy
|
||||||
from authentik.policies.models import PolicyBinding
|
from authentik.policies.models import PolicyBinding
|
||||||
from authentik.providers.oauth2.models import ClientTypes, OAuth2Provider
|
from authentik.providers.oauth2.models import ClientTypes, OAuth2Provider
|
||||||
from tests.e2e.utils import SeleniumTestCase, reconcile_app, retry
|
from tests.e2e.utils import SeleniumTestCase, retry
|
||||||
|
|
||||||
|
|
||||||
@skipUnless(platform.startswith("linux"), "requires local docker")
|
@skipUnless(platform.startswith("linux"), "requires local docker")
|
||||||
|
|
|
@ -8,7 +8,7 @@ from docker.types import Healthcheck
|
||||||
from selenium.webdriver.common.by import By
|
from selenium.webdriver.common.by import By
|
||||||
from selenium.webdriver.support import expected_conditions as ec
|
from selenium.webdriver.support import expected_conditions as ec
|
||||||
|
|
||||||
from authentik.blueprints import apply_blueprint
|
from authentik.blueprints.tests import apply_blueprint, reconcile_app
|
||||||
from authentik.core.models import Application
|
from authentik.core.models import Application
|
||||||
from authentik.core.tests.utils import create_test_cert
|
from authentik.core.tests.utils import create_test_cert
|
||||||
from authentik.flows.models import Flow
|
from authentik.flows.models import Flow
|
||||||
|
@ -21,7 +21,7 @@ from authentik.providers.oauth2.constants import (
|
||||||
SCOPE_OPENID_PROFILE,
|
SCOPE_OPENID_PROFILE,
|
||||||
)
|
)
|
||||||
from authentik.providers.oauth2.models import ClientTypes, OAuth2Provider, ScopeMapping
|
from authentik.providers.oauth2.models import ClientTypes, OAuth2Provider, ScopeMapping
|
||||||
from tests.e2e.utils import SeleniumTestCase, reconcile_app, retry
|
from tests.e2e.utils import SeleniumTestCase, retry
|
||||||
|
|
||||||
|
|
||||||
@skipUnless(platform.startswith("linux"), "requires local docker")
|
@skipUnless(platform.startswith("linux"), "requires local docker")
|
||||||
|
|
|
@ -10,7 +10,7 @@ from docker.types import Healthcheck
|
||||||
from selenium.webdriver.common.by import By
|
from selenium.webdriver.common.by import By
|
||||||
from selenium.webdriver.support import expected_conditions as ec
|
from selenium.webdriver.support import expected_conditions as ec
|
||||||
|
|
||||||
from authentik.blueprints import apply_blueprint
|
from authentik.blueprints.tests import apply_blueprint, reconcile_app
|
||||||
from authentik.core.models import Application
|
from authentik.core.models import Application
|
||||||
from authentik.core.tests.utils import create_test_cert
|
from authentik.core.tests.utils import create_test_cert
|
||||||
from authentik.flows.models import Flow
|
from authentik.flows.models import Flow
|
||||||
|
@ -23,7 +23,7 @@ from authentik.providers.oauth2.constants import (
|
||||||
SCOPE_OPENID_PROFILE,
|
SCOPE_OPENID_PROFILE,
|
||||||
)
|
)
|
||||||
from authentik.providers.oauth2.models import ClientTypes, OAuth2Provider, ScopeMapping
|
from authentik.providers.oauth2.models import ClientTypes, OAuth2Provider, ScopeMapping
|
||||||
from tests.e2e.utils import SeleniumTestCase, reconcile_app, retry
|
from tests.e2e.utils import SeleniumTestCase, retry
|
||||||
|
|
||||||
|
|
||||||
@skipUnless(platform.startswith("linux"), "requires local docker")
|
@skipUnless(platform.startswith("linux"), "requires local docker")
|
||||||
|
|
|
@ -10,7 +10,7 @@ from docker.types import Healthcheck
|
||||||
from selenium.webdriver.common.by import By
|
from selenium.webdriver.common.by import By
|
||||||
from selenium.webdriver.support import expected_conditions as ec
|
from selenium.webdriver.support import expected_conditions as ec
|
||||||
|
|
||||||
from authentik.blueprints import apply_blueprint
|
from authentik.blueprints.tests import apply_blueprint, reconcile_app
|
||||||
from authentik.core.models import Application
|
from authentik.core.models import Application
|
||||||
from authentik.core.tests.utils import create_test_cert
|
from authentik.core.tests.utils import create_test_cert
|
||||||
from authentik.flows.models import Flow
|
from authentik.flows.models import Flow
|
||||||
|
@ -23,7 +23,7 @@ from authentik.providers.oauth2.constants import (
|
||||||
SCOPE_OPENID_PROFILE,
|
SCOPE_OPENID_PROFILE,
|
||||||
)
|
)
|
||||||
from authentik.providers.oauth2.models import ClientTypes, OAuth2Provider, ScopeMapping
|
from authentik.providers.oauth2.models import ClientTypes, OAuth2Provider, ScopeMapping
|
||||||
from tests.e2e.utils import SeleniumTestCase, reconcile_app, retry
|
from tests.e2e.utils import SeleniumTestCase, retry
|
||||||
|
|
||||||
|
|
||||||
@skipUnless(platform.startswith("linux"), "requires local docker")
|
@skipUnless(platform.startswith("linux"), "requires local docker")
|
||||||
|
|
|
@ -11,13 +11,13 @@ from docker.models.containers import Container
|
||||||
from selenium.webdriver.common.by import By
|
from selenium.webdriver.common.by import By
|
||||||
|
|
||||||
from authentik import __version__
|
from authentik import __version__
|
||||||
from authentik.blueprints import apply_blueprint
|
from authentik.blueprints.tests import apply_blueprint, reconcile_app
|
||||||
from authentik.core.models import Application
|
from authentik.core.models import Application
|
||||||
from authentik.flows.models import Flow
|
from authentik.flows.models import Flow
|
||||||
from authentik.outposts.models import DockerServiceConnection, Outpost, OutpostConfig, OutpostType
|
from authentik.outposts.models import DockerServiceConnection, Outpost, OutpostConfig, OutpostType
|
||||||
from authentik.outposts.tasks import outpost_local_connection
|
from authentik.outposts.tasks import outpost_local_connection
|
||||||
from authentik.providers.proxy.models import ProxyProvider
|
from authentik.providers.proxy.models import ProxyProvider
|
||||||
from tests.e2e.utils import SeleniumTestCase, reconcile_app, retry
|
from tests.e2e.utils import SeleniumTestCase, retry
|
||||||
|
|
||||||
|
|
||||||
@skipUnless(platform.startswith("linux"), "requires local docker")
|
@skipUnless(platform.startswith("linux"), "requires local docker")
|
||||||
|
|
|
@ -10,7 +10,7 @@ from docker.types import Healthcheck
|
||||||
from selenium.webdriver.common.by import By
|
from selenium.webdriver.common.by import By
|
||||||
from selenium.webdriver.support import expected_conditions as ec
|
from selenium.webdriver.support import expected_conditions as ec
|
||||||
|
|
||||||
from authentik.blueprints import apply_blueprint
|
from authentik.blueprints.tests import apply_blueprint, reconcile_app
|
||||||
from authentik.core.models import Application
|
from authentik.core.models import Application
|
||||||
from authentik.core.tests.utils import create_test_cert
|
from authentik.core.tests.utils import create_test_cert
|
||||||
from authentik.flows.models import Flow
|
from authentik.flows.models import Flow
|
||||||
|
@ -18,7 +18,7 @@ from authentik.policies.expression.models import ExpressionPolicy
|
||||||
from authentik.policies.models import PolicyBinding
|
from authentik.policies.models import PolicyBinding
|
||||||
from authentik.providers.saml.models import SAMLBindings, SAMLPropertyMapping, SAMLProvider
|
from authentik.providers.saml.models import SAMLBindings, SAMLPropertyMapping, SAMLProvider
|
||||||
from authentik.sources.saml.processors.constants import SAML_BINDING_POST
|
from authentik.sources.saml.processors.constants import SAML_BINDING_POST
|
||||||
from tests.e2e.utils import SeleniumTestCase, reconcile_app, retry
|
from tests.e2e.utils import SeleniumTestCase, retry
|
||||||
|
|
||||||
|
|
||||||
@skipUnless(platform.startswith("linux"), "requires local docker")
|
@skipUnless(platform.startswith("linux"), "requires local docker")
|
||||||
|
|
|
@ -13,7 +13,7 @@ from selenium.webdriver.support import expected_conditions as ec
|
||||||
from selenium.webdriver.support.wait import WebDriverWait
|
from selenium.webdriver.support.wait import WebDriverWait
|
||||||
from yaml import safe_dump
|
from yaml import safe_dump
|
||||||
|
|
||||||
from authentik.blueprints import apply_blueprint
|
from authentik.blueprints.tests import apply_blueprint
|
||||||
from authentik.core.models import User
|
from authentik.core.models import User
|
||||||
from authentik.flows.models import Flow
|
from authentik.flows.models import Flow
|
||||||
from authentik.lib.generators import generate_id, generate_key
|
from authentik.lib.generators import generate_id, generate_key
|
||||||
|
|
|
@ -11,7 +11,7 @@ from selenium.webdriver.common.keys import Keys
|
||||||
from selenium.webdriver.support import expected_conditions as ec
|
from selenium.webdriver.support import expected_conditions as ec
|
||||||
from selenium.webdriver.support.wait import WebDriverWait
|
from selenium.webdriver.support.wait import WebDriverWait
|
||||||
|
|
||||||
from authentik.blueprints import apply_blueprint
|
from authentik.blueprints.tests import apply_blueprint
|
||||||
from authentik.core.models import User
|
from authentik.core.models import User
|
||||||
from authentik.crypto.models import CertificateKeyPair
|
from authentik.crypto.models import CertificateKeyPair
|
||||||
from authentik.flows.models import Flow
|
from authentik.flows.models import Flow
|
||||||
|
|
|
@ -6,7 +6,6 @@ from os import environ, makedirs
|
||||||
from time import sleep, time
|
from time import sleep, time
|
||||||
from typing import Any, Callable, Optional
|
from typing import Any, Callable, Optional
|
||||||
|
|
||||||
from django.apps import apps
|
|
||||||
from django.contrib.staticfiles.testing import StaticLiveServerTestCase
|
from django.contrib.staticfiles.testing import StaticLiveServerTestCase
|
||||||
from django.db import connection
|
from django.db import connection
|
||||||
from django.db.migrations.loader import MigrationLoader
|
from django.db.migrations.loader import MigrationLoader
|
||||||
|
@ -24,7 +23,6 @@ from selenium.webdriver.remote.webelement import WebElement
|
||||||
from selenium.webdriver.support.ui import WebDriverWait
|
from selenium.webdriver.support.ui import WebDriverWait
|
||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
from authentik.blueprints.manager import ManagedAppConfig
|
|
||||||
from authentik.core.api.users import UserSerializer
|
from authentik.core.api.users import UserSerializer
|
||||||
from authentik.core.models import User
|
from authentik.core.models import User
|
||||||
from authentik.core.tests.utils import create_test_admin_user
|
from authentik.core.tests.utils import create_test_admin_user
|
||||||
|
@ -192,24 +190,6 @@ def get_loader():
|
||||||
return MigrationLoader(connection)
|
return MigrationLoader(connection)
|
||||||
|
|
||||||
|
|
||||||
def reconcile_app(app_name: str):
|
|
||||||
"""Re-reconcile AppConfig methods"""
|
|
||||||
|
|
||||||
def wrapper_outer(func: Callable):
|
|
||||||
"""Re-reconcile AppConfig methods"""
|
|
||||||
|
|
||||||
@wraps(func)
|
|
||||||
def wrapper(self: TransactionTestCase, *args, **kwargs):
|
|
||||||
config = apps.get_app_config(app_name)
|
|
||||||
if isinstance(config, ManagedAppConfig):
|
|
||||||
config.reconcile()
|
|
||||||
return func(self, *args, **kwargs)
|
|
||||||
|
|
||||||
return wrapper
|
|
||||||
|
|
||||||
return wrapper_outer
|
|
||||||
|
|
||||||
|
|
||||||
def retry(max_retires=RETRIES, exceptions=None):
|
def retry(max_retires=RETRIES, exceptions=None):
|
||||||
"""Retry test multiple times. Default to catching Selenium Timeout Exception"""
|
"""Retry test multiple times. Default to catching Selenium Timeout Exception"""
|
||||||
|
|
||||||
|
|
|
@ -300,6 +300,9 @@ export class AdminInterface extends LitElement {
|
||||||
<ak-sidebar-item path="/crypto/certificates">
|
<ak-sidebar-item path="/crypto/certificates">
|
||||||
<span slot="label">${t`Certificates`}</span>
|
<span slot="label">${t`Certificates`}</span>
|
||||||
</ak-sidebar-item>
|
</ak-sidebar-item>
|
||||||
|
<ak-sidebar-item path="/blueprints/instances">
|
||||||
|
<span slot="label">${t`Blueprints`}</span>
|
||||||
|
</ak-sidebar-item>
|
||||||
</ak-sidebar-item>
|
</ak-sidebar-item>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,105 @@
|
||||||
|
import { DEFAULT_CONFIG } from "@goauthentik/web/api/Config";
|
||||||
|
import "@goauthentik/web/elements/CodeMirror";
|
||||||
|
import "@goauthentik/web/elements/forms/FormGroup";
|
||||||
|
import "@goauthentik/web/elements/forms/HorizontalFormElement";
|
||||||
|
import { ModelForm } from "@goauthentik/web/elements/forms/ModelForm";
|
||||||
|
import { first } from "@goauthentik/web/utils";
|
||||||
|
import YAML from "yaml";
|
||||||
|
|
||||||
|
import { t } from "@lingui/macro";
|
||||||
|
|
||||||
|
import { TemplateResult, html } from "lit";
|
||||||
|
import { customElement } from "lit/decorators.js";
|
||||||
|
import { until } from "lit/directives/until.js";
|
||||||
|
|
||||||
|
import { BlueprintInstance, ManagedApi } from "@goauthentik/api";
|
||||||
|
|
||||||
|
@customElement("ak-blueprint-form")
|
||||||
|
export class BlueprintForm extends ModelForm<BlueprintInstance, string> {
|
||||||
|
loadInstance(pk: string): Promise<BlueprintInstance> {
|
||||||
|
return new ManagedApi(DEFAULT_CONFIG).managedBlueprintsRetrieve({
|
||||||
|
instanceUuid: pk,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getSuccessMessage(): string {
|
||||||
|
if (this.instance) {
|
||||||
|
return t`Successfully updated instance.`;
|
||||||
|
} else {
|
||||||
|
return t`Successfully created instance.`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
send = (data: BlueprintInstance): Promise<BlueprintInstance> => {
|
||||||
|
if (this.instance?.pk) {
|
||||||
|
return new ManagedApi(DEFAULT_CONFIG).managedBlueprintsUpdate({
|
||||||
|
instanceUuid: this.instance.pk,
|
||||||
|
blueprintInstanceRequest: data,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return new ManagedApi(DEFAULT_CONFIG).managedBlueprintsCreate({
|
||||||
|
blueprintInstanceRequest: data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
renderForm(): TemplateResult {
|
||||||
|
return html`<form class="pf-c-form pf-m-horizontal">
|
||||||
|
<ak-form-element-horizontal label=${t`Name`} ?required=${true} name="name">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value="${first(this.instance?.name)}"
|
||||||
|
class="pf-c-form-control"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</ak-form-element-horizontal>
|
||||||
|
<ak-form-element-horizontal name="enabled">
|
||||||
|
<div class="pf-c-check">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="pf-c-check__input"
|
||||||
|
?checked=${first(this.instance?.enabled, false)}
|
||||||
|
/>
|
||||||
|
<label class="pf-c-check__label"> ${t`Enabled`} </label>
|
||||||
|
</div>
|
||||||
|
<p class="pf-c-form__helper-text">${t`Disabled blueprints are never applied.`}</p>
|
||||||
|
</ak-form-element-horizontal>
|
||||||
|
<ak-form-element-horizontal label=${t`Path`} name="path">
|
||||||
|
<select class="pf-c-form-control">
|
||||||
|
${until(
|
||||||
|
new ManagedApi(DEFAULT_CONFIG)
|
||||||
|
.managedBlueprintsAvailableList()
|
||||||
|
.then((files) => {
|
||||||
|
return files.map((file) => {
|
||||||
|
let name = file.path;
|
||||||
|
if (file.meta && file.meta.name) {
|
||||||
|
name = `${name} (${file.meta.name})`;
|
||||||
|
}
|
||||||
|
const selected = file.path === this.instance?.path;
|
||||||
|
return html`<option ?selected=${selected} value=${file.path}>
|
||||||
|
${name}
|
||||||
|
</option>`;
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
html`<option>${t`Loading...`}</option>`,
|
||||||
|
)}
|
||||||
|
</select>
|
||||||
|
</ak-form-element-horizontal>
|
||||||
|
<ak-form-group>
|
||||||
|
<span slot="header">${t`Additional settings`}</span>
|
||||||
|
<div slot="body" class="pf-c-form">
|
||||||
|
<ak-form-element-horizontal label=${t`Context`} name="context">
|
||||||
|
<ak-codemirror
|
||||||
|
mode="yaml"
|
||||||
|
value="${YAML.stringify(first(this.instance?.context, {}))}"
|
||||||
|
>
|
||||||
|
</ak-codemirror>
|
||||||
|
<p class="pf-c-form__helper-text">
|
||||||
|
${t`Configure the blueprint context, used for templating.`}
|
||||||
|
</p>
|
||||||
|
</ak-form-element-horizontal>
|
||||||
|
</div>
|
||||||
|
</ak-form-group>
|
||||||
|
</form>`;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,148 @@
|
||||||
|
import { AKResponse } from "@goauthentik/web/api/Client";
|
||||||
|
import { DEFAULT_CONFIG } from "@goauthentik/web/api/Config";
|
||||||
|
import { uiConfig } from "@goauthentik/web/common/config";
|
||||||
|
import { EVENT_REFRESH } from "@goauthentik/web/constants";
|
||||||
|
import { PFColor } from "@goauthentik/web/elements/Label";
|
||||||
|
import "@goauthentik/web/elements/buttons/ActionButton";
|
||||||
|
import "@goauthentik/web/elements/buttons/SpinnerButton";
|
||||||
|
import "@goauthentik/web/elements/forms/DeleteBulkForm";
|
||||||
|
import "@goauthentik/web/elements/forms/ModalForm";
|
||||||
|
import { TableColumn } from "@goauthentik/web/elements/table/Table";
|
||||||
|
import { TablePage } from "@goauthentik/web/elements/table/TablePage";
|
||||||
|
import "@goauthentik/web/pages/blueprints/BlueprintForm";
|
||||||
|
|
||||||
|
import { t } from "@lingui/macro";
|
||||||
|
|
||||||
|
import { TemplateResult, html } from "lit";
|
||||||
|
import { customElement, property } from "lit/decorators.js";
|
||||||
|
|
||||||
|
import { BlueprintInstance, BlueprintInstanceStatusEnum, ManagedApi } from "@goauthentik/api";
|
||||||
|
|
||||||
|
export function BlueprintStatus(blueprint?: BlueprintInstance): string {
|
||||||
|
if (!blueprint) return "";
|
||||||
|
switch (blueprint.status) {
|
||||||
|
case BlueprintInstanceStatusEnum.Successful:
|
||||||
|
return t`Successful`;
|
||||||
|
case BlueprintInstanceStatusEnum.Orphaned:
|
||||||
|
return t`Orphaned`;
|
||||||
|
case BlueprintInstanceStatusEnum.Unknown:
|
||||||
|
return t`Unknown`;
|
||||||
|
case BlueprintInstanceStatusEnum.Warning:
|
||||||
|
return t`Warning`;
|
||||||
|
case BlueprintInstanceStatusEnum.Error:
|
||||||
|
return t`Error`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@customElement("ak-blueprint-list")
|
||||||
|
export class BlueprintListPage extends TablePage<BlueprintInstance> {
|
||||||
|
searchEnabled(): boolean {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
pageTitle(): string {
|
||||||
|
return t`Blueprints`;
|
||||||
|
}
|
||||||
|
pageDescription(): string {
|
||||||
|
return t`Automate and template configuration within authentik.`;
|
||||||
|
}
|
||||||
|
pageIcon(): string {
|
||||||
|
return "pf-icon pf-icon-blueprint";
|
||||||
|
}
|
||||||
|
|
||||||
|
checkbox = true;
|
||||||
|
|
||||||
|
@property()
|
||||||
|
order = "name";
|
||||||
|
|
||||||
|
async apiEndpoint(page: number): Promise<AKResponse<BlueprintInstance>> {
|
||||||
|
return new ManagedApi(DEFAULT_CONFIG).managedBlueprintsList({
|
||||||
|
ordering: this.order,
|
||||||
|
page: page,
|
||||||
|
pageSize: (await uiConfig()).pagination.perPage,
|
||||||
|
search: this.search || "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
columns(): TableColumn[] {
|
||||||
|
return [
|
||||||
|
new TableColumn(t`Name`, "name"),
|
||||||
|
new TableColumn(t`Status`, "status"),
|
||||||
|
new TableColumn(t`Last applied`, "last_applied"),
|
||||||
|
new TableColumn(t`Enabled`, "enabled"),
|
||||||
|
new TableColumn(t`Actions`),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
renderToolbarSelected(): TemplateResult {
|
||||||
|
const disabled = this.selectedElements.length < 1;
|
||||||
|
return html`<ak-forms-delete-bulk
|
||||||
|
objectLabel=${t`Blueprint(s)`}
|
||||||
|
.objects=${this.selectedElements}
|
||||||
|
.metadata=${(item: BlueprintInstance) => {
|
||||||
|
return [{ key: t`Name`, value: item.name }];
|
||||||
|
}}
|
||||||
|
.usedBy=${(item: BlueprintInstance) => {
|
||||||
|
return new ManagedApi(DEFAULT_CONFIG).managedBlueprintsUsedByList({
|
||||||
|
instanceUuid: item.pk,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
.delete=${(item: BlueprintInstance) => {
|
||||||
|
return new ManagedApi(DEFAULT_CONFIG).managedBlueprintsDestroy({
|
||||||
|
instanceUuid: item.pk,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<button ?disabled=${disabled} slot="trigger" class="pf-c-button pf-m-danger">
|
||||||
|
${t`Delete`}
|
||||||
|
</button>
|
||||||
|
</ak-forms-delete-bulk>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
row(item: BlueprintInstance): TemplateResult[] {
|
||||||
|
return [
|
||||||
|
html`${item.name}`,
|
||||||
|
html`${BlueprintStatus(item)}`,
|
||||||
|
html`${item.lastApplied.toLocaleString()}`,
|
||||||
|
html`<ak-label color=${item.enabled ? PFColor.Green : PFColor.Red}>
|
||||||
|
${item.enabled ? t`Yes` : t`No`}
|
||||||
|
</ak-label>`,
|
||||||
|
html`<ak-action-button
|
||||||
|
class="pf-m-plain"
|
||||||
|
.apiRequest=${() => {
|
||||||
|
return new ManagedApi(DEFAULT_CONFIG)
|
||||||
|
.managedBlueprintsApplyCreate({
|
||||||
|
instanceUuid: item.pk,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
this.dispatchEvent(
|
||||||
|
new CustomEvent(EVENT_REFRESH, {
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<i class="fas fa-play" aria-hidden="true"></i>
|
||||||
|
</ak-action-button>
|
||||||
|
<ak-forms-modal>
|
||||||
|
<span slot="submit"> ${t`Update`} </span>
|
||||||
|
<span slot="header"> ${t`Update Blueprint`} </span>
|
||||||
|
<ak-blueprint-form slot="form" .instancePk=${item.pk}> </ak-blueprint-form>
|
||||||
|
<button slot="trigger" class="pf-c-button pf-m-plain">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</button>
|
||||||
|
</ak-forms-modal>`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
renderObjectCreate(): TemplateResult {
|
||||||
|
return html`
|
||||||
|
<ak-forms-modal>
|
||||||
|
<span slot="submit"> ${t`Create`} </span>
|
||||||
|
<span slot="header"> ${t`Create Blueprint Instance`} </span>
|
||||||
|
<ak-blueprint-form slot="form"> </ak-blueprint-form>
|
||||||
|
<button slot="trigger" class="pf-c-button pf-m-primary">${t`Create`}</button>
|
||||||
|
</ak-forms-modal>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
|
@ -124,7 +124,7 @@ export class SystemTaskListPage extends TablePage<Task> {
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<i class="fas fa-sync-alt" aria-hidden="true"></i>
|
<i class="fas fa-play" aria-hidden="true"></i>
|
||||||
</ak-action-button>`,
|
</ak-action-button>`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
@ -129,4 +129,8 @@ export const ROUTES: Route[] = [
|
||||||
await import("@goauthentik/web/pages/crypto/CertificateKeyPairListPage");
|
await import("@goauthentik/web/pages/crypto/CertificateKeyPairListPage");
|
||||||
return html`<ak-crypto-certificate-list></ak-crypto-certificate-list>`;
|
return html`<ak-crypto-certificate-list></ak-crypto-certificate-list>`;
|
||||||
}),
|
}),
|
||||||
|
new Route(new RegExp("^/blueprints/instances$"), async () => {
|
||||||
|
await import("@goauthentik/web/pages/blueprints/BlueprintListPage");
|
||||||
|
return html`<ak-blueprint-list></ak-blueprint-list>`;
|
||||||
|
}),
|
||||||
];
|
];
|
||||||
|
|
Reference in New Issue