blueprints: support setting file URLs in blueprints (#5510)

* blueprints: support setting file URLs in blueprints

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* make new fields not required

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* include conditional fields in schema

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* update docs

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
Jens L 2023-05-08 15:07:00 +02:00 committed by GitHub
parent af9766972d
commit 9f4be4d150
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 314 additions and 82 deletions

View file

@ -9,7 +9,7 @@ from rest_framework.fields import Field, JSONField, UUIDField
from rest_framework.serializers import Serializer
from structlog.stdlib import get_logger
from authentik.blueprints.v1.importer import is_model_allowed
from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT, is_model_allowed
from authentik.blueprints.v1.meta.registry import registry
from authentik.lib.models import SerializerModel
@ -81,7 +81,11 @@ class Command(BaseCommand):
model_instance: Model = model()
if not isinstance(model_instance, SerializerModel):
continue
serializer = model_instance.serializer()
serializer = model_instance.serializer(
context={
SERIALIZER_CONTEXT_BLUEPRINT: False,
}
)
model_path = f"{model._meta.app_label}.{model._meta.model_name}"
self.schema["properties"]["entries"]["items"]["oneOf"].append(
self.template_entry(model_path, serializer)
@ -96,7 +100,7 @@ class Command(BaseCommand):
self.schema["$defs"][def_name] = model_schema
return {
"type": "object",
"required": ["model", "attrs"],
"required": ["model", "identifiers"],
"properties": {
"model": {"const": model_path},
"id": {"type": "string"},

View file

@ -0,0 +1,41 @@
version: 1
metadata:
name: test conditional fields
labels:
blueprints.goauthentik.io/description: |
Some models have conditional fields that are only allowed in blueprint contexts
- Token (key)
- Application (icon)
- Source (icon)
- Flow (background)
entries:
- model: authentik_core.token
identifiers:
identifier: %(uid)s-token
attrs:
key: %(uid)s
user: %(user)s
intent: api
- model: authentik_core.application
identifiers:
slug: %(uid)s-app
attrs:
name: %(uid)s-app
icon: https://goauthentik.io/img/icon.png
- model: authentik_sources_oauth.oauthsource
identifiers:
slug: %(uid)s-source
attrs:
name: %(uid)s-source
provider_type: azuread
consumer_key: %(uid)s
consumer_secret: %(uid)s
icon: https://goauthentik.io/img/icon.png
- model: authentik_flows.flow
identifiers:
slug: %(uid)s-flow
attrs:
name: %(uid)s-flow
title: %(uid)s-flow
designation: authentication
background: https://goauthentik.io/img/icon.png

View file

@ -0,0 +1,47 @@
"""Test blueprints v1"""
from django.test import TransactionTestCase
from authentik.blueprints.v1.importer import Importer
from authentik.core.models import Application, Token
from authentik.core.tests.utils import create_test_admin_user
from authentik.flows.models import Flow
from authentik.lib.generators import generate_id
from authentik.lib.tests.utils import load_fixture
from authentik.sources.oauth.models import OAuthSource
class TestBlueprintsV1ConditionalFields(TransactionTestCase):
"""Test Blueprints conditional fields"""
def setUp(self) -> None:
user = create_test_admin_user()
self.uid = generate_id()
import_yaml = load_fixture("fixtures/conditional_fields.yaml", uid=self.uid, user=user.pk)
importer = Importer(import_yaml)
self.assertTrue(importer.validate()[0])
self.assertTrue(importer.apply())
def test_token(self):
"""Test token"""
token = Token.objects.filter(identifier=f"{self.uid}-token").first()
self.assertIsNotNone(token)
self.assertEqual(token.key, self.uid)
def test_application(self):
"""Test application"""
app = Application.objects.filter(slug=f"{self.uid}-app").first()
self.assertIsNotNone(app)
self.assertEqual(app.meta_icon, "https://goauthentik.io/img/icon.png")
def test_source(self):
"""Test source"""
source = OAuthSource.objects.filter(slug=f"{self.uid}-source").first()
self.assertIsNotNone(source)
self.assertEqual(source.icon, "https://goauthentik.io/img/icon.png")
def test_flow(self):
"""Test flow"""
flow = Flow.objects.filter(slug=f"{self.uid}-flow").first()
self.assertIsNotNone(flow)
self.assertEqual(flow.background, "https://goauthentik.io/img/icon.png")

View file

@ -11,7 +11,7 @@ from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema
from guardian.shortcuts import get_objects_for_user
from rest_framework.decorators import action
from rest_framework.fields import ReadOnlyField, SerializerMethodField
from rest_framework.fields import CharField, ReadOnlyField, SerializerMethodField
from rest_framework.parsers import MultiPartParser
from rest_framework.request import Request
from rest_framework.response import Response
@ -23,6 +23,7 @@ from structlog.testing import capture_logs
from authentik.admin.api.metrics import CoordinateSerializer
from authentik.api.decorators import permission_required
from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT
from authentik.core.api.providers import ProviderSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.models import Application, User
@ -61,6 +62,11 @@ class ApplicationSerializer(ModelSerializer):
user = self.context["request"].user
return app.get_launch_url(user)
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
if SERIALIZER_CONTEXT_BLUEPRINT in self.context:
self.fields["icon"] = CharField(source="meta_icon", required=False)
class Meta:
model = Application
fields = [

View file

@ -5,16 +5,18 @@ from django_filters.rest_framework import DjangoFilterBackend
from drf_spectacular.utils import OpenApiResponse, extend_schema
from rest_framework import mixins
from rest_framework.decorators import action
from rest_framework.fields import CharField, ReadOnlyField, SerializerMethodField
from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.parsers import MultiPartParser
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer, ReadOnlyField, SerializerMethodField
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import GenericViewSet
from structlog.stdlib import get_logger
from authentik.api.authorization import OwnerFilter, OwnerSuperuserPermissions
from authentik.api.decorators import permission_required
from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import MetaNameSerializer, TypeCreateSerializer
from authentik.core.models import Source, UserSourceConnection
@ -44,6 +46,11 @@ class SourceSerializer(ModelSerializer, MetaNameSerializer):
return ""
return obj.component
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
if SERIALIZER_CONTEXT_BLUEPRINT in self.context:
self.fields["icon"] = CharField(required=False)
class Meta:
model = Source
fields = [

View file

@ -6,7 +6,7 @@ from django.utils.translation import gettext as _
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiResponse, extend_schema
from rest_framework.decorators import action
from rest_framework.fields import BooleanField, DictField, ListField, ReadOnlyField
from rest_framework.fields import BooleanField, CharField, DictField, ListField, ReadOnlyField
from rest_framework.parsers import MultiPartParser
from rest_framework.request import Request
from rest_framework.response import Response
@ -16,7 +16,7 @@ from structlog.stdlib import get_logger
from authentik.api.decorators import permission_required
from authentik.blueprints.v1.exporter import FlowExporter
from authentik.blueprints.v1.importer import Importer
from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT, Importer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import CacheSerializer, LinkSerializer, PassiveSerializer
from authentik.events.utils import sanitize_dict
@ -52,6 +52,11 @@ class FlowSerializer(ModelSerializer):
"""Get export URL for flow"""
return reverse("authentik_api:flow-export", kwargs={"slug": flow.slug})
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
if SERIALIZER_CONTEXT_BLUEPRINT in self.context:
self.fields["background"] = CharField(required=False)
class Meta:
model = Flow
fields = [

View file

@ -45,7 +45,7 @@
"type": "object",
"required": [
"model",
"attrs"
"identifiers"
],
"properties": {
"model": {
@ -81,7 +81,7 @@
"type": "object",
"required": [
"model",
"attrs"
"identifiers"
],
"properties": {
"model": {
@ -117,7 +117,7 @@
"type": "object",
"required": [
"model",
"attrs"
"identifiers"
],
"properties": {
"model": {
@ -153,7 +153,7 @@
"type": "object",
"required": [
"model",
"attrs"
"identifiers"
],
"properties": {
"model": {
@ -189,7 +189,7 @@
"type": "object",
"required": [
"model",
"attrs"
"identifiers"
],
"properties": {
"model": {
@ -225,7 +225,7 @@
"type": "object",
"required": [
"model",
"attrs"
"identifiers"
],
"properties": {
"model": {
@ -261,7 +261,7 @@
"type": "object",
"required": [
"model",
"attrs"
"identifiers"
],
"properties": {
"model": {
@ -297,7 +297,7 @@
"type": "object",
"required": [
"model",
"attrs"
"identifiers"
],
"properties": {
"model": {
@ -333,7 +333,7 @@
"type": "object",
"required": [
"model",
"attrs"
"identifiers"
],
"properties": {
"model": {
@ -369,7 +369,7 @@
"type": "object",
"required": [
"model",
"attrs"
"identifiers"
],
"properties": {
"model": {
@ -405,7 +405,7 @@
"type": "object",
"required": [
"model",
"attrs"
"identifiers"
],
"properties": {
"model": {
@ -441,7 +441,7 @@
"type": "object",
"required": [
"model",
"attrs"
"identifiers"
],
"properties": {
"model": {
@ -477,7 +477,7 @@
"type": "object",
"required": [
"model",
"attrs"
"identifiers"
],
"properties": {
"model": {
@ -513,7 +513,7 @@
"type": "object",
"required": [
"model",
"attrs"
"identifiers"
],
"properties": {
"model": {
@ -549,7 +549,7 @@
"type": "object",
"required": [
"model",
"attrs"
"identifiers"
],
"properties": {
"model": {
@ -585,7 +585,7 @@
"type": "object",
"required": [
"model",
"attrs"
"identifiers"
],
"properties": {
"model": {
@ -621,7 +621,7 @@
"type": "object",
"required": [
"model",
"attrs"
"identifiers"
],
"properties": {
"model": {
@ -657,7 +657,7 @@
"type": "object",
"required": [
"model",
"attrs"
"identifiers"
],
"properties": {
"model": {
@ -693,7 +693,7 @@
"type": "object",
"required": [
"model",
"attrs"
"identifiers"
],
"properties": {
"model": {
@ -729,7 +729,7 @@
"type": "object",
"required": [
"model",
"attrs"
"identifiers"
],
"properties": {
"model": {
@ -765,7 +765,7 @@
"type": "object",
"required": [
"model",
"attrs"
"identifiers"
],
"properties": {
"model": {
@ -801,7 +801,7 @@
"type": "object",
"required": [
"model",
"attrs"
"identifiers"
],
"properties": {
"model": {
@ -837,7 +837,7 @@
"type": "object",
"required": [
"model",
"attrs"
"identifiers"
],
"properties": {
"model": {
@ -873,7 +873,7 @@
"type": "object",
"required": [
"model",
"attrs"
"identifiers"
],
"properties": {
"model": {
@ -909,7 +909,7 @@
"type": "object",
"required": [
"model",
"attrs"
"identifiers"
],
"properties": {
"model": {
@ -945,7 +945,7 @@
"type": "object",
"required": [
"model",
"attrs"
"identifiers"
],
"properties": {
"model": {
@ -981,7 +981,7 @@
"type": "object",
"required": [
"model",
"attrs"
"identifiers"
],
"properties": {
"model": {
@ -1017,7 +1017,7 @@
"type": "object",
"required": [
"model",
"attrs"
"identifiers"
],
"properties": {
"model": {
@ -1053,7 +1053,7 @@
"type": "object",
"required": [
"model",
"attrs"
"identifiers"
],
"properties": {
"model": {
@ -1089,7 +1089,7 @@
"type": "object",
"required": [
"model",
"attrs"
"identifiers"
],
"properties": {
"model": {
@ -1125,7 +1125,7 @@
"type": "object",
"required": [
"model",
"attrs"
"identifiers"
],
"properties": {
"model": {
@ -1161,7 +1161,7 @@
"type": "object",
"required": [
"model",
"attrs"
"identifiers"
],
"properties": {
"model": {
@ -1197,7 +1197,7 @@
"type": "object",
"required": [
"model",
"attrs"
"identifiers"
],
"properties": {
"model": {
@ -1233,7 +1233,7 @@
"type": "object",
"required": [
"model",
"attrs"
"identifiers"
],
"properties": {
"model": {
@ -1269,7 +1269,7 @@
"type": "object",
"required": [
"model",
"attrs"
"identifiers"
],
"properties": {
"model": {
@ -1305,7 +1305,7 @@
"type": "object",
"required": [
"model",
"attrs"
"identifiers"
],
"properties": {
"model": {
@ -1341,7 +1341,7 @@
"type": "object",
"required": [
"model",
"attrs"
"identifiers"
],
"properties": {
"model": {
@ -1377,7 +1377,7 @@
"type": "object",
"required": [
"model",
"attrs"
"identifiers"
],
"properties": {
"model": {
@ -1413,7 +1413,7 @@
"type": "object",
"required": [
"model",
"attrs"
"identifiers"
],
"properties": {
"model": {
@ -1449,7 +1449,7 @@
"type": "object",
"required": [
"model",
"attrs"
"identifiers"
],
"properties": {
"model": {
@ -1485,7 +1485,7 @@
"type": "object",
"required": [
"model",
"attrs"
"identifiers"
],
"properties": {
"model": {
@ -1521,7 +1521,7 @@
"type": "object",
"required": [
"model",
"attrs"
"identifiers"
],
"properties": {
"model": {
@ -1557,7 +1557,7 @@
"type": "object",
"required": [
"model",
"attrs"
"identifiers"
],
"properties": {
"model": {
@ -1593,7 +1593,7 @@
"type": "object",
"required": [
"model",
"attrs"
"identifiers"
],
"properties": {
"model": {
@ -1629,7 +1629,7 @@
"type": "object",
"required": [
"model",
"attrs"
"identifiers"
],
"properties": {
"model": {
@ -1665,7 +1665,7 @@
"type": "object",
"required": [
"model",
"attrs"
"identifiers"
],
"properties": {
"model": {
@ -1701,7 +1701,7 @@
"type": "object",
"required": [
"model",
"attrs"
"identifiers"
],
"properties": {
"model": {
@ -1737,7 +1737,7 @@
"type": "object",
"required": [
"model",
"attrs"
"identifiers"
],
"properties": {
"model": {
@ -1773,7 +1773,7 @@
"type": "object",
"required": [
"model",
"attrs"
"identifiers"
],
"properties": {
"model": {
@ -1809,7 +1809,7 @@
"type": "object",
"required": [
"model",
"attrs"
"identifiers"
],
"properties": {
"model": {
@ -1845,7 +1845,7 @@
"type": "object",
"required": [
"model",
"attrs"
"identifiers"
],
"properties": {
"model": {
@ -1881,7 +1881,7 @@
"type": "object",
"required": [
"model",
"attrs"
"identifiers"
],
"properties": {
"model": {
@ -1917,7 +1917,7 @@
"type": "object",
"required": [
"model",
"attrs"
"identifiers"
],
"properties": {
"model": {
@ -1953,7 +1953,7 @@
"type": "object",
"required": [
"model",
"attrs"
"identifiers"
],
"properties": {
"model": {
@ -1989,7 +1989,7 @@
"type": "object",
"required": [
"model",
"attrs"
"identifiers"
],
"properties": {
"model": {
@ -2025,7 +2025,7 @@
"type": "object",
"required": [
"model",
"attrs"
"identifiers"
],
"properties": {
"model": {
@ -2061,7 +2061,7 @@
"type": "object",
"required": [
"model",
"attrs"
"identifiers"
],
"properties": {
"model": {
@ -2097,7 +2097,7 @@
"type": "object",
"required": [
"model",
"attrs"
"identifiers"
],
"properties": {
"model": {
@ -2133,7 +2133,7 @@
"type": "object",
"required": [
"model",
"attrs"
"identifiers"
],
"properties": {
"model": {
@ -2169,7 +2169,7 @@
"type": "object",
"required": [
"model",
"attrs"
"identifiers"
],
"properties": {
"model": {
@ -2205,7 +2205,7 @@
"type": "object",
"required": [
"model",
"attrs"
"identifiers"
],
"properties": {
"model": {
@ -2241,7 +2241,7 @@
"type": "object",
"required": [
"model",
"attrs"
"identifiers"
],
"properties": {
"model": {
@ -2277,7 +2277,7 @@
"type": "object",
"required": [
"model",
"attrs"
"identifiers"
],
"properties": {
"model": {
@ -2313,7 +2313,7 @@
"type": "object",
"required": [
"model",
"attrs"
"identifiers"
],
"properties": {
"model": {
@ -2349,7 +2349,7 @@
"type": "object",
"required": [
"model",
"attrs"
"identifiers"
],
"properties": {
"model": {
@ -2385,7 +2385,7 @@
"type": "object",
"required": [
"model",
"attrs"
"identifiers"
],
"properties": {
"model": {
@ -2421,7 +2421,7 @@
"type": "object",
"required": [
"model",
"attrs"
"identifiers"
],
"properties": {
"model": {
@ -2457,7 +2457,7 @@
"type": "object",
"required": [
"model",
"attrs"
"identifiers"
],
"properties": {
"model": {
@ -2493,7 +2493,7 @@
"type": "object",
"required": [
"model",
"attrs"
"identifiers"
],
"properties": {
"model": {
@ -2529,7 +2529,7 @@
"type": "object",
"required": [
"model",
"attrs"
"identifiers"
],
"properties": {
"model": {
@ -2867,6 +2867,11 @@
"title": "Designation",
"description": "Decides what this Flow is used for. For example, the Authentication flow is redirect to when an un-authenticated user visits authentik."
},
"background": {
"type": "string",
"minLength": 1,
"title": "Background"
},
"policy_engine_mode": {
"type": "string",
"enum": [
@ -4675,6 +4680,11 @@
"minLength": 1,
"title": "User path template"
},
"icon": {
"type": "string",
"minLength": 1,
"title": "Icon"
},
"server_uri": {
"type": "string",
"minLength": 1,
@ -4858,6 +4868,11 @@
"minLength": 1,
"title": "User path template"
},
"icon": {
"type": "string",
"minLength": 1,
"title": "Icon"
},
"provider_type": {
"type": "string",
"enum": [
@ -4966,6 +4981,11 @@
"null"
],
"title": "Access token"
},
"icon": {
"type": "string",
"minLength": 1,
"title": "Icon"
}
},
"required": []
@ -5026,6 +5046,11 @@
"minLength": 1,
"title": "User path template"
},
"icon": {
"type": "string",
"minLength": 1,
"title": "Icon"
},
"client_id": {
"type": "string",
"minLength": 1,
@ -5068,6 +5093,11 @@
"type": "string",
"minLength": 1,
"title": "Plex token"
},
"icon": {
"type": "string",
"minLength": 1,
"title": "Icon"
}
},
"required": []
@ -5128,6 +5158,11 @@
"minLength": 1,
"title": "User path template"
},
"icon": {
"type": "string",
"minLength": 1,
"title": "Icon"
},
"pre_authentication_flow": {
"type": "integer",
"title": "Pre authentication flow",
@ -5228,6 +5263,11 @@
"type": "string",
"minLength": 1,
"title": "Identifier"
},
"icon": {
"type": "string",
"minLength": 1,
"title": "Icon"
}
},
"required": []
@ -8187,6 +8227,11 @@
"group": {
"type": "string",
"title": "Group"
},
"icon": {
"type": "string",
"minLength": 1,
"title": "Icon"
}
},
"required": []
@ -8236,6 +8281,11 @@
"expiring": {
"type": "boolean",
"title": "Expiring"
},
"key": {
"type": "string",
"minLength": 1,
"title": "Key"
}
},
"required": []

View file

@ -15,7 +15,7 @@ Blueprints are yaml files, whose format is described further in [File structure]
- As a Blueprint instance, which is a YAML file mounted into the authentik (worker) container. This file is read and applied regularly (every 60 minutes). Multiple instances can be created for a single blueprint file, and instances can be given context key:value attributes to configure the blueprint.
:::info
Starting with authentik 2022.12.1, authentik listens for file modification/creation events in the blueprint directory and will automatically trigger a discovery when a new blueprint file is created, and trigger a blueprint apply when a file is modified.
Starting with authentik 2022.12.1, authentik watches for file modification/creation events in the blueprint directory and will automatically trigger a discovery when a new blueprint file is created, and trigger a blueprint apply when a file is modified.
:::
- As a Flow import, which is a YAML file uploaded via the Browser/API. This file is validated and applied directly after being uploaded, but is not further monitored/applied.

View file

@ -1,6 +1,6 @@
# Meta models
Since blueprints have a pretty strict mapping of each entry mapping to an instance of a model in the database, _meta models_ have been added to trigger other actions within authentik that don't directly map to a model.
Since blueprints have a pretty strict mapping of each entry mapping to an instance of a model in the database, _meta models_ exist to trigger other actions within authentik that don't directly map to a model.
### `authentik_blueprints.metaapplyblueprint`

View file

@ -25,3 +25,65 @@ For example:
user: !KeyOf my-user
intent: api
```
### `authentik_core.application`
:::info
Requires authentik 2023.5
:::
Application icons can be directly set to URLs with the `icon` field.
For example:
```yaml
# [...]
- model: authentik_core.application
identifiers:
slug: my-app
attrs:
name: My App
icon: https://goauthentik.io/img/icon.png
```
### `authentik_sources_oauth.oauthsource`, `authentik_sources_saml.samlsource`, `authentik_sources_plex.plexsource`
:::info
Requires authentik 2023.5
:::
Source icons can be directly set to URLs with the `icon` field.
For example:
```yaml
# [...]
- model: authentik_sources_oauth.oauthsource
identifiers:
slug: my-source
attrs:
name: My source
icon: https://goauthentik.io/img/icon.png
```
### `authentik_flows.flow`
:::info
Requires authentik 2023.5
:::
Flow backgrounds can be directly set to URLs with the `background` field.
For example:
```yaml
# [...]
- model: authentik_flows.flow
identifiers:
slug: my-flow
attrs:
name: my-flow
title: My flow
designation: authentication
background: https://goauthentik.io/img/icon.png
```

View file

@ -1,6 +1,5 @@
---
title: Flow Context
toc_max_heading_level: 5
---
Each flow execution has an independent _context_. This context holds all of the arbitrary data about that specific flow, data which can then be used and transformed by stages and policies.

View file

@ -117,6 +117,9 @@ module.exports = {
],
copyright: `Copyright © ${new Date().getFullYear()} Authentik Security Inc. Built with Docusaurus.`,
},
tableOfContents: {
maxHeadingLevel: 5,
},
colorMode: {
respectPrefersColorScheme: true,
},

View file

@ -49,6 +49,7 @@ module.exports = {
},
footer: mainConfig.themeConfig.footer,
colorMode: mainConfig.themeConfig.colorMode,
tableOfContents: mainConfig.themeConfig.tableOfContents,
},
presets: [
[

View file

@ -16,8 +16,15 @@ module.exports = {
"blueprints/v1/structure",
"blueprints/v1/tags",
"blueprints/v1/example",
"blueprints/v1/models",
"blueprints/v1/meta",
{
type: "category",
label: "Models",
link: {
type: "doc",
id: "blueprints/v1/models",
},
items: ["blueprints/v1/meta"],
},
],
},
{