diff --git a/authentik/admin/views/outposts.py b/authentik/admin/views/outposts.py index 0b93715cf..c9772ce8b 100644 --- a/authentik/admin/views/outposts.py +++ b/authentik/admin/views/outposts.py @@ -12,10 +12,7 @@ from django.utils.translation import gettext as _ from django.views.generic import UpdateView from guardian.mixins import PermissionRequiredMixin -from authentik.admin.views.utils import ( - BackSuccessUrlMixin, - DeleteMessageView, -) +from authentik.admin.views.utils import BackSuccessUrlMixin, DeleteMessageView from authentik.lib.views import CreateAssignPermView from authentik.outposts.forms import OutpostForm from authentik.outposts.models import Outpost, OutpostConfig diff --git a/authentik/outposts/api/outposts.py b/authentik/outposts/api/outposts.py index c3c054620..db9b08ce9 100644 --- a/authentik/outposts/api/outposts.py +++ b/authentik/outposts/api/outposts.py @@ -51,6 +51,13 @@ class OutpostViewSet(ModelViewSet): queryset = Outpost.objects.all() serializer_class = OutpostSerializer + filterset_fields = { + "providers": ["isnull"], + } + search_fields = [ + "name", + "providers__name", + ] @swagger_auto_schema(responses={200: OutpostHealthSerializer(many=True)}) @action(methods=["GET"], detail=True) diff --git a/swagger.yaml b/swagger.yaml index 75d42d415..75b1575e3 100755 --- a/swagger.yaml +++ b/swagger.yaml @@ -1789,6 +1789,11 @@ paths: operationId: outposts_outposts_list description: Outpost Viewset parameters: + - name: providers__isnull + in: query + description: '' + required: false + type: string - name: ordering in: query description: Which field to use when ordering the results. @@ -1914,11 +1919,11 @@ paths: /outposts/outposts/{uuid}/health/: get: operationId: outposts_outposts_health - description: Outpost Viewset + description: Get outposts current health parameters: [] responses: '200': - description: '' + description: Outpost health status schema: description: '' type: array @@ -4457,7 +4462,7 @@ paths: /providers/oauth2/{id}/setup_urls/: get: operationId: providers_oauth2_setup_urls - description: Return metadata as XML string + description: Get Providers setup URLs parameters: [] responses: '200': @@ -8179,11 +8184,15 @@ definitions: type: string format: uuid x-nullable: true + token_identifier: + title: Token identifier + type: string + readOnly: true _config: title: config type: object OutpostHealth: - description: '' + description: Outpost health status type: object properties: last_seen: @@ -8191,6 +8200,20 @@ definitions: type: string format: date-time readOnly: true + version: + title: Version + type: string + readOnly: true + minLength: 1 + version_should: + title: Version should + type: string + readOnly: true + minLength: 1 + version_outdated: + title: Version outdated + type: boolean + readOnly: true OpenIDConnectConfiguration: title: Oidc configuration description: rest_framework Serializer for OIDC Configuration diff --git a/web/src/api/Outposts.ts b/web/src/api/Outposts.ts new file mode 100644 index 000000000..53a172769 --- /dev/null +++ b/web/src/api/Outposts.ts @@ -0,0 +1,39 @@ +import { DefaultClient, PBResponse, QueryArguments } from "./Client"; +import { Provider } from "./Providers"; + +export interface OutpostHealth { + last_seen: number; + version: string; + version_should: string; + version_outdated: boolean; +} + +export class Outpost { + + pk: string; + name: string; + providers: Provider[]; + service_connection?: string; + _config: QueryArguments; + token_identifier: string; + + constructor() { + throw Error(); + } + + static get(pk: string): Promise { + return DefaultClient.fetch(["outposts", "outposts", pk]); + } + + static list(filter?: QueryArguments): Promise> { + return DefaultClient.fetch>(["outposts", "outposts"], filter); + } + + static health(pk: string): Promise { + return DefaultClient.fetch(["outposts", "outposts", pk, "health"]); + } + + static adminUrl(rest: string): string { + return `/administration/outposts/${rest}`; + } +} diff --git a/web/src/interfaces/AdminInterface.ts b/web/src/interfaces/AdminInterface.ts index a6482ea20..00c437176 100644 --- a/web/src/interfaces/AdminInterface.ts +++ b/web/src/interfaces/AdminInterface.ts @@ -27,7 +27,7 @@ export const SIDEBAR_ITEMS: SidebarItem[] = [ `^/sources/(?${SLUG_REGEX})$`, ), new SidebarItem("Providers", "/providers"), - new SidebarItem("Outposts", "/administration/outposts/"), + new SidebarItem("Outposts", "/outposts"), new SidebarItem("Outpost Service Connections", "/administration/outpost_service_connections/"), ).when((): Promise => { return User.me().then(u => u.is_superuser); diff --git a/web/src/pages/outposts/OutpostHealth.ts b/web/src/pages/outposts/OutpostHealth.ts new file mode 100644 index 000000000..7d15542f2 --- /dev/null +++ b/web/src/pages/outposts/OutpostHealth.ts @@ -0,0 +1,49 @@ +import { gettext } from "django"; +import { CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element"; +import { until } from "lit-html/directives/until"; +import { Outpost } from "../../api/Outposts"; +import { COMMON_STYLES } from "../../common/styles"; + +@customElement("ak-outpost-health") +export class OutpostHealth extends LitElement { + + @property() + outpostId?: string; + + static get styles(): CSSResult[] { + return COMMON_STYLES; + } + + render(): TemplateResult { + if (!this.outpostId) { + return html``; + } + return html`
    ${until(Outpost.health(this.outpostId).then((oh) => { + if (oh.length === 0) { + return html`
  • +
      +
    • +  ${gettext("Not available")} +
    • +
    +
  • `; + } + return oh.map((h) => { + return html`
  • +
      +
    • +  ${gettext(`Last seen: ${new Date(h.last_seen * 1000).toLocaleTimeString()}`)} +
    • +
    • + ${h.version_outdated ? + html`  + ${gettext(`${h.version}, should be ${h.version_should}`)}` : + html` ${gettext(`Version: ${h.version}`)}`} +
    • +
    +
  • `; + }); + }), html``)}
`; + } + +} diff --git a/web/src/pages/outposts/OutpostListPage.ts b/web/src/pages/outposts/OutpostListPage.ts new file mode 100644 index 000000000..7caa96d9a --- /dev/null +++ b/web/src/pages/outposts/OutpostListPage.ts @@ -0,0 +1,121 @@ +import { gettext } from "django"; +import { customElement, property } from "lit-element"; +import { html, TemplateResult } from "lit-html"; +import { PBResponse } from "../../api/Client"; +import { Outpost } 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"; + +@customElement("ak-outpost-list") +export class OutpostListPage extends TablePage { + pageTitle(): string { + return "Outposts"; + } + pageDescription(): string | undefined { + return "Outposts are deployments of authentik components to support different environments and protocols, like reverse proxies."; + } + pageIcon(): string { + return "pf-icon pf-icon-zone"; + } + searchEnabled(): boolean { + return true; + } + apiEndpoint(page: number): Promise> { + return Outpost.list({ + ordering: this.order, + page: page, + search: this.search || "", + }); + } + columns(): TableColumn[] { + return [ + new TableColumn("Name", "name"), + new TableColumn("Providers"), + new TableColumn("Health and Version"), + new TableColumn(""), + ]; + } + + @property() + order = "name"; + + row(item: Outpost): TemplateResult[] { + return [ + html`${item.name}`, + html`
    ${item.providers.map((p) => { + return html`
  • ${p.name}
  • `; + })}
`, + html``, + html` + + + ${gettext("Edit")} + +
+
+ + + ${gettext("Delete")} + +
+
+ + +
+
+

${gettext('Outpost Deployment Info')}

+
+ + +
+
`, + ]; + } + + renderToolbar(): TemplateResult { + return html` + + + ${gettext("Create")} + +
+
+ ${super.renderToolbar()} + `; + } +} diff --git a/web/src/routes.ts b/web/src/routes.ts index 32c97b71b..3237b3046 100644 --- a/web/src/routes.ts +++ b/web/src/routes.ts @@ -14,6 +14,7 @@ import "./pages/events/RuleListPage"; import "./pages/providers/ProviderListPage"; import "./pages/providers/ProviderViewPage"; import "./pages/property-mappings/PropertyMappingListPage"; +import "./pages/outposts/OutpostListPage"; export const ROUTES: Route[] = [ // Prevent infinite Shell loops @@ -42,4 +43,5 @@ export const ROUTES: Route[] = [ new Route(new RegExp("^/events/transports$"), html``), new Route(new RegExp("^/events/rules$"), html``), new Route(new RegExp("^/property-mappings$"), html``), + new Route(new RegExp("^/outposts$"), html``), ];