web: migrate Outpost Service Connection to web

This commit is contained in:
Jens Langhammer 2021-02-19 17:49:34 +01:00
parent 8008918d8b
commit 865f652476
11 changed files with 353 additions and 219 deletions

View file

@ -1,153 +0,0 @@
{% extends "administration/base.html" %}
{% load i18n %}
{% load humanize %}
{% load authentik_utils %}
{% block content %}
<section class="pf-c-page__main-section pf-m-light">
<div class="pf-c-content">
<h1>
<i class="pf-icon-integration"></i>
{% trans 'Outpost Service-Connections' %}
</h1>
<p>{% trans "Outpost Service-Connections define how authentik connects to external platforms to manage and deploy Outposts." %}</p>
</div>
</section>
<section class="pf-c-page__main-section pf-m-no-padding-mobile">
<div class="pf-c-card">
{% if object_list %}
<div class="pf-c-toolbar">
<div class="pf-c-toolbar__content">
{% include 'partials/toolbar_search.html' %}
<div class="pf-c-toolbar__bulk-select">
<ak-dropdown class="pf-c-dropdown">
<button class="pf-m-primary pf-c-dropdown__toggle" type="button">
<span class="pf-c-dropdown__toggle-text">{% trans 'Create' %}</span>
<i class="fas fa-caret-down pf-c-dropdown__toggle-icon" aria-hidden="true"></i>
</button>
<ul class="pf-c-dropdown__menu" hidden>
{% for type, name in types.items %}
<li>
<ak-modal-button href="{% url 'authentik_admin:outpost-service-connection-create' %}?type={{ type }}">
<button slot="trigger" class="pf-c-dropdown__menu-item">
{{ name|verbose_name }}<br>
<small>
{{ name|doc }}
</small>
</button>
<div slot="modal"></div>
</ak-modal-button>
</li>
{% endfor %}
</ul>
</ak-dropdown>
<button role="ak-refresh" class="pf-c-button pf-m-primary">
{% trans 'Refresh' %}
</button>
</div>
{% include 'partials/pagination.html' %}
</div>
</div>
<table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid">
<thead>
<tr role="row">
<th role="columnheader" scope="col">{% trans 'Name' %}</th>
<th role="columnheader" scope="col">{% trans 'Type' %}</th>
<th role="columnheader" scope="col">{% trans 'Local?' %}</th>
<th role="columnheader" scope="col">{% trans 'Status' %}</th>
<th role="cell"></th>
</tr>
</thead>
<tbody role="rowgroup">
{% for sc in object_list %}
<tr role="row">
<th role="columnheader">
<span>{{ sc.name }}</span>
</th>
<td role="cell">
<span>
{{ sc|verbose_name }}
</span>
</td>
<td role="cell">
<span>
{{ sc.local|yesno:"Yes,No" }}
</span>
</td>
<td role="cell">
<span>
{% if sc.state.healthy %}
<i class="fas fa-check pf-m-success"></i> {{ sc.state.version }}
{% else %}
<i class="fas fa-times pf-m-danger"></i> {% trans 'Unhealthy' %}
{% endif %}
</span>
</td>
<td>
<ak-modal-button href="{% url 'authentik_admin:outpost-service-connection-update' pk=sc.pk %}">
<ak-spinner-button slot="trigger" class="pf-m-secondary">
{% trans 'Edit' %}
</ak-spinner-button>
<div slot="modal"></div>
</ak-modal-button>
<ak-modal-button href="{% url 'authentik_admin:outpost-service-connection-delete' pk=sc.pk %}">
<ak-spinner-button slot="trigger" class="pf-m-danger">
{% trans 'Delete' %}
</ak-spinner-button>
<div slot="modal"></div>
</ak-modal-button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="pf-c-pagination pf-m-bottom">
{% include 'partials/pagination.html' %}
</div>
{% else %}
<div class="pf-c-toolbar">
<div class="pf-c-toolbar__content">
{% include 'partials/toolbar_search.html' %}
</div>
</div>
<div class="pf-c-empty-state">
<div class="pf-c-empty-state__content">
<i class="fas fa-map-marker pf-c-empty-state__icon" aria-hidden="true"></i>
<h1 class="pf-c-title pf-m-lg">
{% trans 'No Outpost Service Connections.' %}
</h1>
<div class="pf-c-empty-state__body">
{% if request.GET.search != "" %}
{% trans "Your search query doesn't match any outposts." %}
{% else %}
{% trans 'Currently no service connections exist. Click the button below to create one.' %}
{% endif %}
</div>
<ak-dropdown class="pf-c-dropdown">
<button class="pf-m-primary pf-c-dropdown__toggle" type="button">
<span class="pf-c-dropdown__toggle-text">{% trans 'Create' %}</span>
<i class="fas fa-caret-down pf-c-dropdown__toggle-icon" aria-hidden="true"></i>
</button>
<ul class="pf-c-dropdown__menu" hidden>
{% for type, name in types.items %}
<li>
<ak-modal-button href="{% url 'authentik_admin:outpost-service-connection-create' %}?type={{ type }}">
<button slot="trigger" class="pf-c-dropdown__menu-item">
{{ name|verbose_name }}<br>
<small>
{{ name|doc }}
</small>
</button>
<div slot="modal"></div>
</ak-modal-button>
</li>
{% endfor %}
</ul>
</ak-dropdown>
</div>
</div>
{% endif %}
</div>
</section>
{% endblock %}

View file

@ -308,11 +308,6 @@ urlpatterns = [
name="outpost-delete",
),
# Outpost Service Connections
path(
"outpost_service_connections/",
outposts_service_connections.OutpostServiceConnectionListView.as_view(),
name="outpost-service-connections",
),
path(
"outpost_service_connections/create/",
outposts_service_connections.OutpostServiceConnectionCreateView.as_view(),

View file

@ -4,38 +4,18 @@ from django.contrib.auth.mixins import (
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
)
from django.contrib.messages.views import SuccessMessageMixin
from django.urls import reverse_lazy
from django.utils.translation import gettext as _
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
from guardian.mixins import PermissionRequiredMixin
from authentik.admin.views.utils import (
BackSuccessUrlMixin,
DeleteMessageView,
InheritanceCreateView,
InheritanceListView,
InheritanceUpdateView,
SearchListMixin,
UserPaginateListMixin,
)
from authentik.outposts.models import OutpostServiceConnection
class OutpostServiceConnectionListView(
LoginRequiredMixin,
PermissionListMixin,
UserPaginateListMixin,
SearchListMixin,
InheritanceListView,
):
"""Show list of all outpost-service-connections"""
model = OutpostServiceConnection
permission_required = "authentik_outposts.add_outpostserviceconnection"
template_name = "administration/outpost_service_connection/list.html"
ordering = "pk"
search_fields = ["pk", "name"]
class OutpostServiceConnectionCreateView(
SuccessMessageMixin,
BackSuccessUrlMixin,
@ -49,8 +29,8 @@ class OutpostServiceConnectionCreateView(
permission_required = "authentik_outposts.add_outpostserviceconnection"
template_name = "generic/create.html"
success_url = reverse_lazy("authentik_admin:outpost-service-connections")
success_message = _("Successfully created OutpostServiceConnection")
success_url = "/"
success_message = _("Successfully created Outpost Service Connection")
class OutpostServiceConnectionUpdateView(
@ -66,8 +46,8 @@ class OutpostServiceConnectionUpdateView(
permission_required = "authentik_outposts.change_outpostserviceconnection"
template_name = "generic/update.html"
success_url = reverse_lazy("authentik_admin:outpost-service-connections")
success_message = _("Successfully updated OutpostServiceConnection")
success_url = "/"
success_message = _("Successfully updated Outpost Service Connection")
class OutpostServiceConnectionDeleteView(
@ -79,5 +59,5 @@ class OutpostServiceConnectionDeleteView(
permission_required = "authentik_outposts.delete_outpostserviceconnection"
template_name = "generic/delete.html"
success_url = reverse_lazy("authentik_admin:outpost-service-connections")
success_message = _("Successfully deleted OutpostServiceConnection")
success_url = "/"
success_message = _("Successfully deleted Outpost Service Connection")

View file

@ -1,7 +1,19 @@
"""Outpost API Views"""
from rest_framework.serializers import ModelSerializer
from dataclasses import asdict
from django.db.models.base import Model
from django.shortcuts import reverse
from drf_yasg2.utils import swagger_auto_schema
from rest_framework.decorators import action
from rest_framework.fields import BooleanField, CharField, SerializerMethodField
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer, Serializer
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.utils import MetaNameSerializer, TypeCreateSerializer
from authentik.lib.templatetags.authentik_utils import verbose_name
from authentik.lib.utils.reflection import all_subclasses
from authentik.outposts.models import (
DockerServiceConnection,
KubernetesServiceConnection,
@ -9,32 +21,79 @@ from authentik.outposts.models import (
)
class ServiceConnectionSerializer(ModelSerializer):
class ServiceConnectionSerializer(ModelSerializer, MetaNameSerializer):
"""ServiceConnection Serializer"""
object_type = SerializerMethodField()
def get_object_type(self, obj: OutpostServiceConnection) -> str:
"""Get object type so that we know which API Endpoint to use to get the full object"""
return obj._meta.object_name.lower().replace("serviceconnection", "")
class Meta:
model = OutpostServiceConnection
fields = ["pk", "name"]
fields = [
"pk",
"name",
"local",
"object_type",
"verbose_name",
"verbose_name_plural",
]
class ServiceConnectionStateSerializer(Serializer):
"""Serializer for Service connection state"""
healthy = BooleanField(read_only=True)
version = CharField(read_only=True)
def create(self, validated_data: dict) -> Model:
raise NotImplementedError
def update(self, instance: Model, validated_data: dict) -> Model:
raise NotImplementedError
class ServiceConnectionViewSet(ModelViewSet):
"""ServiceConnection Viewset"""
queryset = OutpostServiceConnection.objects.all()
queryset = OutpostServiceConnection.objects.select_subclasses()
serializer_class = ServiceConnectionSerializer
@swagger_auto_schema(responses={200: TypeCreateSerializer(many=True)})
@action(detail=False)
def types(self, request: Request) -> Response:
"""Get all creatable service connection types"""
data = []
for subclass in all_subclasses(self.queryset.model):
data.append(
{
"name": verbose_name(subclass),
"description": subclass.__doc__,
"link": reverse("authentik_admin:outpost-service-connection-create")
+ f"?type={subclass.__name__}",
}
)
return Response(TypeCreateSerializer(data, many=True).data)
class DockerServiceConnectionSerializer(ModelSerializer):
@swagger_auto_schema(responses={200: ServiceConnectionStateSerializer(many=False)})
@action(detail=True)
# pylint: disable=unused-argument, invalid-name
def state(self, request: Request, pk: str) -> Response:
"""Get the service connection's state"""
connection = self.get_object()
return Response(asdict(connection.state))
class DockerServiceConnectionSerializer(ServiceConnectionSerializer):
"""DockerServiceConnection Serializer"""
class Meta:
model = DockerServiceConnection
fields = [
"pk",
"name",
"local",
fields = ServiceConnectionSerializer.Meta.fields + [
"url",
"tls_verification",
"tls_authentication",
@ -48,13 +107,13 @@ class DockerServiceConnectionViewSet(ModelViewSet):
serializer_class = DockerServiceConnectionSerializer
class KubernetesServiceConnectionSerializer(ModelSerializer):
class KubernetesServiceConnectionSerializer(ServiceConnectionSerializer):
"""KubernetesServiceConnection Serializer"""
class Meta:
model = KubernetesServiceConnection
fields = ["pk", "name", "local", "kubeconfig"]
fields = ServiceConnectionSerializer.Meta.fields + ["kubeconfig"]
class KubernetesServiceConnectionViewSet(ModelViewSet):

View file

@ -2169,6 +2169,42 @@ paths:
tags:
- outposts
parameters: []
/outposts/service_connections/all/types/:
get:
operationId: outposts_service_connections_all_types
description: Get all creatable service connection types
parameters:
- name: ordering
in: query
description: Which field to use when ordering the results.
required: false
type: string
- name: search
in: query
description: A search term.
required: false
type: string
- name: page
in: query
description: A page number within the paginated result set.
required: false
type: integer
- name: page_size
in: query
description: Number of results to return per page.
required: false
type: integer
responses:
'200':
description: Types of an object that can be created
schema:
description: ''
type: array
items:
$ref: '#/definitions/TypeCreate'
tags:
- outposts
parameters: []
/outposts/service_connections/all/{uuid}/:
get:
operationId: outposts_service_connections_all_read
@ -2229,6 +2265,25 @@ paths:
required: true
type: string
format: uuid
/outposts/service_connections/all/{uuid}/state/:
get:
operationId: outposts_service_connections_all_state
description: Get the service connection's state
parameters: []
responses:
'200':
description: Serializer for Service connection state
schema:
$ref: '#/definitions/ServiceConnectionState'
tags:
- outposts
parameters:
- name: uuid
in: path
description: A UUID string identifying this Outpost Service-Connection.
required: true
type: string
format: uuid
/outposts/service_connections/docker/:
get:
operationId: outposts_service_connections_docker_list
@ -8797,6 +8852,55 @@ definitions:
title: Name
type: string
minLength: 1
local:
title: Local
description: If enabled, use the local connection. Required Docker socket/Kubernetes
Integration
type: boolean
object_type:
title: Object type
type: string
readOnly: true
verbose_name:
title: Verbose name
type: string
readOnly: true
verbose_name_plural:
title: Verbose name plural
type: string
readOnly: true
TypeCreate:
description: Types of an object that can be created
type: object
properties:
name:
title: Name
type: string
readOnly: true
minLength: 1
description:
title: Description
type: string
readOnly: true
minLength: 1
link:
title: Link
type: string
readOnly: true
minLength: 1
ServiceConnectionState:
description: Serializer for Service connection state
type: object
properties:
healthy:
title: Healthy
type: boolean
readOnly: true
version:
title: Version
type: string
readOnly: true
minLength: 1
DockerServiceConnection:
description: DockerServiceConnection Serializer
required:
@ -8818,6 +8922,18 @@ definitions:
description: If enabled, use the local connection. Required Docker socket/Kubernetes
Integration
type: boolean
object_type:
title: Object type
type: string
readOnly: true
verbose_name:
title: Verbose name
type: string
readOnly: true
verbose_name_plural:
title: Verbose name plural
type: string
readOnly: true
url:
title: Url
description: Can be in the format of 'unix://<path>' when connecting to a
@ -8859,6 +8975,18 @@ definitions:
description: If enabled, use the local connection. Required Docker socket/Kubernetes
Integration
type: boolean
object_type:
title: Object type
type: string
readOnly: true
verbose_name:
title: Verbose name
type: string
readOnly: true
verbose_name_plural:
title: Verbose name plural
type: string
readOnly: true
kubeconfig:
title: Kubeconfig
description: Paste your kubeconfig here. authentik will automatically use
@ -8898,25 +9026,6 @@ definitions:
title: Bound to
type: integer
readOnly: true
TypeCreate:
description: Types of an object that can be created
type: object
properties:
name:
title: Name
type: string
readOnly: true
minLength: 1
description:
title: Description
type: string
readOnly: true
minLength: 1
link:
title: Link
type: string
readOnly: true
minLength: 1
PolicyBinding:
description: PolicyBinding Serializer
required:

View file

@ -1,5 +1,5 @@
import { DefaultClient, AKResponse, QueryArguments } from "./Client";
import { Provider } from "./Providers";
import { Provider, TypeCreate } from "./Providers";
export interface OutpostHealth {
last_seen: number;
@ -38,3 +38,42 @@ export class Outpost {
return `/administration/outposts/${rest}`;
}
}
export interface OutpostServiceConnectionState {
version: string;
healthy: boolean;
}
export class OutpostServiceConnection {
pk: string;
name: string;
local: boolean;
object_type: string;
verbose_name: string;
verbose_name_plural: string;
constructor() {
throw Error();
}
static get(pk: string): Promise<OutpostServiceConnection> {
return DefaultClient.fetch<OutpostServiceConnection>(["outposts", "service_connections", "all", pk]);
}
static list(filter?: QueryArguments): Promise<AKResponse<OutpostServiceConnection>> {
return DefaultClient.fetch<AKResponse<OutpostServiceConnection>>(["outposts", "service_connections", "all"], filter);
}
static state(pk: string): Promise<OutpostServiceConnectionState> {
return DefaultClient.fetch<OutpostServiceConnectionState>(["outposts", "service_connections", "all", pk, "state"]);
}
static getTypes(): Promise<TypeCreate[]> {
return DefaultClient.fetch<TypeCreate[]>(["outposts", "service_connections", "all", "types"]);
}
static adminUrl(rest: string): string {
return `/administration/outpost_service_connections/${rest}`;
}
}

View file

@ -28,7 +28,7 @@ export const SIDEBAR_ITEMS: SidebarItem[] = [
),
new SidebarItem("Providers", "/providers"),
new SidebarItem("Outposts", "/outposts"),
new SidebarItem("Outpost Service Connections", "/administration/outpost_service_connections/"),
new SidebarItem("Outpost Service Connections", "/outpost-service-connections"),
).when((): Promise<boolean> => {
return User.me().then(u => u.is_superuser);
}),

View file

@ -7,6 +7,7 @@ import "../../elements/Tabs";
import "../../elements/AdminLoginsChart";
import "../../elements/buttons/ModalButton";
import "../../elements/buttons/SpinnerButton";
import "../../elements/buttons/Dropdown";
import "../../elements/policies/BoundPoliciesList";
import { FlowStageBinding, Stage } from "../../api/Flows";
import { until } from "lit-html/directives/until";

View file

@ -0,0 +1,102 @@
import { gettext } from "django";
import { customElement, property } from "lit-element";
import { html, TemplateResult } from "lit-html";
import { AKResponse } from "../../api/Client";
import { OutpostServiceConnection } from "../../api/Outposts";
import { TableColumn } from "../../elements/table/Table";
import { TablePage } from "../../elements/table/TablePage";
import "./OutpostHealth";
import "../../elements/buttons/SpinnerButton";
import "../../elements/buttons/ModalButton";
import "../../elements/buttons/Dropdown";
import { until } from "lit-html/directives/until";
@customElement("ak-outpost-service-connection-list")
export class OutpostServiceConnectionListPage extends TablePage<OutpostServiceConnection> {
pageTitle(): string {
return "Outpost Service-Connections";
}
pageDescription(): string | undefined {
return "Outpost Service-Connections define how authentik connects to external platforms to manage and deploy Outposts.";
}
pageIcon(): string {
return "pf-icon pf-icon-integration";
}
searchEnabled(): boolean {
return true;
}
apiEndpoint(page: number): Promise<AKResponse<OutpostServiceConnection>> {
return OutpostServiceConnection.list({
ordering: this.order,
page: page,
search: this.search || "",
});
}
columns(): TableColumn[] {
return [
new TableColumn("Name", "name"),
new TableColumn("Type"),
new TableColumn("Local", "local"),
new TableColumn("State"),
new TableColumn(""),
];
}
@property()
order = "name";
row(item: OutpostServiceConnection): TemplateResult[] {
return [
html`${item.name}`,
html`${item.verbose_name}`,
html`${item.local ? "Yes" : "No"}`,
html`${until(OutpostServiceConnection.state(item.pk).then((state) => {
if (state.healthy) {
return html`<i class="fas fa-check pf-m-success"></i> ${state.version}`;
}
return html`<i class="fas fa-times pf-m-danger"></i> ${gettext("Unhealthy")}`;
}), html`<ak-spinner></ak-spinner>`)}`,
html`
<ak-modal-button href="${OutpostServiceConnection.adminUrl(`${item.pk}/update/`)}">
<ak-spinner-button slot="trigger" class="pf-m-secondary">
${gettext("Edit")}
</ak-spinner-button>
<div slot="modal"></div>
</ak-modal-button>
<ak-modal-button href="${OutpostServiceConnection.adminUrl(`${item.pk}/delete/`)}">
<ak-spinner-button slot="trigger" class="pf-m-danger">
${gettext("Delete")}
</ak-spinner-button>
<div slot="modal"></div>
</ak-modal-button>`,
];
}
renderToolbar(): TemplateResult {
return html`
<ak-dropdown class="pf-c-dropdown">
<button class="pf-m-primary pf-c-dropdown__toggle" type="button">
<span class="pf-c-dropdown__toggle-text">${gettext("Create")}</span>
<i class="fas fa-caret-down pf-c-dropdown__toggle-icon" aria-hidden="true"></i>
</button>
<ul class="pf-c-dropdown__menu" hidden>
${until(OutpostServiceConnection.getTypes().then((types) => {
return types.map((type) => {
return html`<li>
<ak-modal-button href="${type.link}">
<button slot="trigger" class="pf-c-dropdown__menu-item">${type.name}<br>
<small>${type.description}</small>
</button>
<div slot="modal"></div>
</ak-modal-button>
</li>`;
});
}), html`<ak-spinner></ak-spinner>`)}
</ul>
</ak-dropdown>
${super.renderToolbar()}`;
}
}

View file

@ -31,7 +31,7 @@ export class PolicyListPage extends TablePage<Policy> {
apiEndpoint(page: number): Promise<AKResponse<Policy>> {
return Policy.list({
ordering: this.order,
page: page,
page: page,
search: this.search || "",
});
}
@ -54,7 +54,7 @@ export class PolicyListPage extends TablePage<Policy> {
${gettext(`Assigned to ${item.bound_to} objects.`)}
</small>`:
html`<i class="pf-icon pf-icon-warning-triangle"></i>
<small>${gettext("Warning: Policy is not assigned.")}/small>`}
<small>${gettext("Warning: Policy is not assigned.")}</small>`}
</div>`,
html`${item.verbose_name}`,
html`

View file

@ -13,6 +13,7 @@ import "./pages/flows/FlowListPage";
import "./pages/flows/FlowViewPage";
import "./pages/LibraryPage";
import "./pages/outposts/OutpostListPage";
import "./pages/outposts/OutpostServiceConnectionListPage";
import "./pages/policies/PolicyListPage";
import "./pages/property-mappings/PropertyMappingListPage";
import "./pages/providers/ProviderListPage";
@ -53,5 +54,6 @@ export const ROUTES: Route[] = [
new Route(new RegExp("^/events/rules$"), html`<ak-event-rule-list></ak-event-rule-list>`),
new Route(new RegExp("^/property-mappings$"), html`<ak-property-mapping-list></ak-property-mapping-list>`),
new Route(new RegExp("^/outposts$"), html`<ak-outpost-list></ak-outpost-list>`),
new Route(new RegExp("^/outpost-service-connections$"), html`<ak-outpost-service-connection-list></ak-outpost-service-connection-list>`),
new Route(new RegExp("^/crypto/certificates$"), html`<ak-crypto-certificatekeypair-list></ak-crypto-certificatekeypair-list>`),
];