web: re-organise frontend and cleanup common code (#3572)
* fix repo in api client Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * web: re-organise files to match their interface Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * core: include version in script tags Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * cleanup maybe broken Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * revert rename Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * web: get rid of Client.ts Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * move more to common Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * more moving Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * format Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * unfuck files that vscode fucked, thanks Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * move more Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * finish moving (maybe) Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * ok more moving Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * fix more stuff that vs code destroyed Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * get rid "web" prefix for virtual package Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * fix locales Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * use custom base element Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * fix css file Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * don't run autoDetectLanguage when importing locale Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * fix circular dependencies Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * web: fix build Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
		
							
								
								
									
										312
									
								
								web/src/admin/AdminInterface.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										312
									
								
								web/src/admin/AdminInterface.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,312 @@ | ||||
| import { ROUTES } from "@goauthentik/admin/Routes"; | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { | ||||
|     EVENT_API_DRAWER_TOGGLE, | ||||
|     EVENT_NOTIFICATION_DRAWER_TOGGLE, | ||||
|     EVENT_SIDEBAR_TOGGLE, | ||||
|     VERSION, | ||||
| } from "@goauthentik/common/constants"; | ||||
| import { autoDetectLanguage } from "@goauthentik/common/ui/locale"; | ||||
| import { me } from "@goauthentik/common/users"; | ||||
| import { WebsocketClient } from "@goauthentik/common/ws"; | ||||
| import { AKElement } from "@goauthentik/elements/Base"; | ||||
| import "@goauthentik/elements/messages/MessageContainer"; | ||||
| import "@goauthentik/elements/messages/MessageContainer"; | ||||
| import "@goauthentik/elements/notifications/NotificationDrawer"; | ||||
| import { ID_REGEX, SLUG_REGEX, UUID_REGEX } from "@goauthentik/elements/router/Route"; | ||||
| import { getURLParam, updateURLParams } from "@goauthentik/elements/router/RouteMatch"; | ||||
| import "@goauthentik/elements/router/RouterOutlet"; | ||||
| import "@goauthentik/elements/sidebar/Sidebar"; | ||||
| import "@goauthentik/elements/sidebar/SidebarItem"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { CSSResult, TemplateResult, css, html } from "lit"; | ||||
| import { customElement, property } from "lit/decorators.js"; | ||||
| import { until } from "lit/directives/until.js"; | ||||
|  | ||||
| import AKGlobal from "@goauthentik/common/styles/authentik.css"; | ||||
| import PFButton from "@patternfly/patternfly/components/Button/button.css"; | ||||
| import PFDrawer from "@patternfly/patternfly/components/Drawer/drawer.css"; | ||||
| import PFPage from "@patternfly/patternfly/components/Page/page.css"; | ||||
| import PFBase from "@patternfly/patternfly/patternfly-base.css"; | ||||
|  | ||||
| import { AdminApi, Version } from "@goauthentik/api"; | ||||
|  | ||||
| autoDetectLanguage(); | ||||
|  | ||||
| @customElement("ak-interface-admin") | ||||
| export class AdminInterface extends AKElement { | ||||
|     @property({ type: Boolean }) | ||||
|     sidebarOpen = true; | ||||
|  | ||||
|     @property({ type: Boolean }) | ||||
|     notificationDrawerOpen = getURLParam("notificationDrawerOpen", false); | ||||
|  | ||||
|     @property({ type: Boolean }) | ||||
|     apiDrawerOpen = getURLParam("apiDrawerOpen", false); | ||||
|  | ||||
|     ws: WebsocketClient; | ||||
|  | ||||
|     private version: Promise<Version>; | ||||
|  | ||||
|     static get styles(): CSSResult[] { | ||||
|         return [ | ||||
|             PFBase, | ||||
|             PFPage, | ||||
|             PFButton, | ||||
|             PFDrawer, | ||||
|             AKGlobal, | ||||
|             css` | ||||
|                 .pf-c-page__main, | ||||
|                 .pf-c-drawer__content, | ||||
|                 .pf-c-page__drawer { | ||||
|                     z-index: auto !important; | ||||
|                     background-color: transparent; | ||||
|                 } | ||||
|                 .display-none { | ||||
|                     display: none; | ||||
|                 } | ||||
|                 .pf-c-page { | ||||
|                     background-color: var(--pf-c-page--BackgroundColor) !important; | ||||
|                 } | ||||
|                 @media (prefers-color-scheme: dark) { | ||||
|                     /* Global page background colour */ | ||||
|                     .pf-c-page { | ||||
|                         --pf-c-page--BackgroundColor: var(--ak-dark-background); | ||||
|                     } | ||||
|                 } | ||||
|             `, | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     constructor() { | ||||
|         super(); | ||||
|         this.ws = new WebsocketClient(); | ||||
|         this.sidebarOpen = window.innerWidth >= 1280; | ||||
|         window.addEventListener("resize", () => { | ||||
|             this.sidebarOpen = window.innerWidth >= 1280; | ||||
|         }); | ||||
|         window.addEventListener(EVENT_SIDEBAR_TOGGLE, () => { | ||||
|             this.sidebarOpen = !this.sidebarOpen; | ||||
|         }); | ||||
|         window.addEventListener(EVENT_NOTIFICATION_DRAWER_TOGGLE, () => { | ||||
|             this.notificationDrawerOpen = !this.notificationDrawerOpen; | ||||
|             updateURLParams({ | ||||
|                 notificationDrawerOpen: this.notificationDrawerOpen, | ||||
|             }); | ||||
|         }); | ||||
|         window.addEventListener(EVENT_API_DRAWER_TOGGLE, () => { | ||||
|             this.apiDrawerOpen = !this.apiDrawerOpen; | ||||
|             updateURLParams({ | ||||
|                 apiDrawerOpen: this.apiDrawerOpen, | ||||
|             }); | ||||
|         }); | ||||
|         this.version = new AdminApi(DEFAULT_CONFIG).adminVersionRetrieve(); | ||||
|         me().then((u) => { | ||||
|             if (!u.user.isSuperuser && u.user.pk > 0) { | ||||
|                 window.location.assign("/if/user"); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     render(): TemplateResult { | ||||
|         return html` <div class="pf-c-page"> | ||||
|             <ak-sidebar | ||||
|                 class="pf-c-page__sidebar ${this.sidebarOpen ? "pf-m-expanded" : "pf-m-collapsed"}" | ||||
|             > | ||||
|                 ${this.renderSidebarItems()} | ||||
|             </ak-sidebar> | ||||
|             <div class="pf-c-page__drawer"> | ||||
|                 <div | ||||
|                     class="pf-c-drawer ${this.notificationDrawerOpen || this.apiDrawerOpen | ||||
|                         ? "pf-m-expanded" | ||||
|                         : "pf-m-collapsed"}" | ||||
|                 > | ||||
|                     <div class="pf-c-drawer__main"> | ||||
|                         <div class="pf-c-drawer__content"> | ||||
|                             <div class="pf-c-drawer__body"> | ||||
|                                 <main class="pf-c-page__main"> | ||||
|                                     <ak-router-outlet | ||||
|                                         role="main" | ||||
|                                         class="pf-c-page__main" | ||||
|                                         tabindex="-1" | ||||
|                                         id="main-content" | ||||
|                                         defaultUrl="/administration/overview" | ||||
|                                         .routes=${ROUTES} | ||||
|                                     > | ||||
|                                     </ak-router-outlet> | ||||
|                                 </main> | ||||
|                             </div> | ||||
|                         </div> | ||||
|                         <ak-notification-drawer | ||||
|                             class="pf-c-drawer__panel pf-m-width-33 ${this.notificationDrawerOpen | ||||
|                                 ? "" | ||||
|                                 : "display-none"}" | ||||
|                             ?hidden=${!this.notificationDrawerOpen} | ||||
|                         ></ak-notification-drawer> | ||||
|                         <ak-api-drawer | ||||
|                             class="pf-c-drawer__panel pf-m-width-33 ${this.apiDrawerOpen | ||||
|                                 ? "" | ||||
|                                 : "display-none"}" | ||||
|                             ?hidden=${!this.apiDrawerOpen} | ||||
|                         ></ak-api-drawer> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </div> | ||||
|         </div>`; | ||||
|     } | ||||
|  | ||||
|     renderSidebarItems(): TemplateResult { | ||||
|         return html` | ||||
|             ${until( | ||||
|                 this.version.then((version) => { | ||||
|                     if (version.versionCurrent !== VERSION) { | ||||
|                         return html`<ak-sidebar-item ?highlight=${true}> | ||||
|                             <span slot="label" | ||||
|                                 >${t`A newer version of the frontend is available.`}</span | ||||
|                             > | ||||
|                         </ak-sidebar-item>`; | ||||
|                     } | ||||
|                     return html``; | ||||
|                 }), | ||||
|             )} | ||||
|             ${until( | ||||
|                 me().then((u) => { | ||||
|                     if (u.original) { | ||||
|                         return html`<ak-sidebar-item | ||||
|                             ?highlight=${true} | ||||
|                             ?isAbsoluteLink=${true} | ||||
|                             path=${`/-/impersonation/end/?back=${encodeURIComponent( | ||||
|                                 `${window.location.pathname}#${window.location.hash}`, | ||||
|                             )}`} | ||||
|                         > | ||||
|                             <span slot="label" | ||||
|                                 >${t`You're currently impersonating ${u.user.username}. Click to stop.`}</span | ||||
|                             > | ||||
|                         </ak-sidebar-item>`; | ||||
|                     } | ||||
|                     return html``; | ||||
|                 }), | ||||
|             )} | ||||
|             <ak-sidebar-item path="/if/user/" ?isAbsoluteLink=${true} ?highlight=${true}> | ||||
|                 <span slot="label">${t`User interface`}</span> | ||||
|             </ak-sidebar-item> | ||||
|             <ak-sidebar-item .expanded=${true}> | ||||
|                 <span slot="label">${t`Dashboards`}</span> | ||||
|                 <ak-sidebar-item path="/administration/overview"> | ||||
|                     <span slot="label">${t`Overview`}</span> | ||||
|                 </ak-sidebar-item> | ||||
|                 <ak-sidebar-item path="/administration/dashboard/users"> | ||||
|                     <span slot="label">${t`Users`}</span> | ||||
|                 </ak-sidebar-item> | ||||
|                 <ak-sidebar-item path="/administration/system-tasks"> | ||||
|                     <span slot="label">${t`System Tasks`}</span> | ||||
|                 </ak-sidebar-item> | ||||
|             </ak-sidebar-item> | ||||
|             <ak-sidebar-item> | ||||
|                 <span slot="label">${t`Applications`}</span> | ||||
|                 <ak-sidebar-item | ||||
|                     path="/core/applications" | ||||
|                     .activeWhen=${[`^/core/applications/(?<slug>${SLUG_REGEX})$`]} | ||||
|                 > | ||||
|                     <span slot="label">${t`Applications`}</span> | ||||
|                 </ak-sidebar-item> | ||||
|                 <ak-sidebar-item | ||||
|                     path="/core/providers" | ||||
|                     .activeWhen=${[`^/core/providers/(?<id>${ID_REGEX})$`]} | ||||
|                 > | ||||
|                     <span slot="label">${t`Providers`}</span> | ||||
|                 </ak-sidebar-item> | ||||
|                 <ak-sidebar-item path="/outpost/outposts"> | ||||
|                     <span slot="label">${t`Outposts`}</span> | ||||
|                 </ak-sidebar-item> | ||||
|             </ak-sidebar-item> | ||||
|             <ak-sidebar-item> | ||||
|                 <span slot="label">${t`Events`}</span> | ||||
|                 <ak-sidebar-item | ||||
|                     path="/events/log" | ||||
|                     .activeWhen=${[`^/events/log/(?<id>${UUID_REGEX})$`]} | ||||
|                 > | ||||
|                     <span slot="label">${t`Logs`}</span> | ||||
|                 </ak-sidebar-item> | ||||
|                 <ak-sidebar-item path="/events/rules"> | ||||
|                     <span slot="label">${t`Notification Rules`}</span> | ||||
|                 </ak-sidebar-item> | ||||
|                 <ak-sidebar-item path="/events/transports"> | ||||
|                     <span slot="label">${t`Notification Transports`}</span> | ||||
|                 </ak-sidebar-item> | ||||
|             </ak-sidebar-item> | ||||
|             <ak-sidebar-item> | ||||
|                 <span slot="label">${t`Customisation`}</span> | ||||
|                 <ak-sidebar-item path="/policy/policies"> | ||||
|                     <span slot="label">${t`Policies`}</span> | ||||
|                 </ak-sidebar-item> | ||||
|                 <ak-sidebar-item path="/policy/reputation"> | ||||
|                     <span slot="label">${t`Reputation scores`}</span> | ||||
|                 </ak-sidebar-item> | ||||
|                 <ak-sidebar-item path="/core/property-mappings"> | ||||
|                     <span slot="label">${t`Property Mappings`}</span> | ||||
|                 </ak-sidebar-item> | ||||
|                 <ak-sidebar-item path="/blueprints/instances"> | ||||
|                     <span slot="label">${t`Blueprints`}</span> | ||||
|                 </ak-sidebar-item> | ||||
|             </ak-sidebar-item> | ||||
|             <ak-sidebar-item> | ||||
|                 <span slot="label">${t`Flows & Stages`}</span> | ||||
|                 <ak-sidebar-item | ||||
|                     path="/flow/flows" | ||||
|                     .activeWhen=${[`^/flow/flows/(?<slug>${SLUG_REGEX})$`]} | ||||
|                 > | ||||
|                     <span slot="label">${t`Flows`}</span> | ||||
|                 </ak-sidebar-item> | ||||
|                 <ak-sidebar-item path="/flow/stages"> | ||||
|                     <span slot="label">${t`Stages`}</span> | ||||
|                 </ak-sidebar-item> | ||||
|                 <ak-sidebar-item path="/flow/stages/prompts"> | ||||
|                     <span slot="label">${t`Prompts`}</span> | ||||
|                 </ak-sidebar-item> | ||||
|             </ak-sidebar-item> | ||||
|             <ak-sidebar-item> | ||||
|                 <span slot="label">${t`Directory`}</span> | ||||
|                 <ak-sidebar-item | ||||
|                     path=${`/identity/users;${encodeURIComponent( | ||||
|                         JSON.stringify({ | ||||
|                             path: "users", | ||||
|                         }), | ||||
|                     )}`} | ||||
|                     .activeWhen=${[`^/identity/users/(?<id>${ID_REGEX})$`]} | ||||
|                 > | ||||
|                     <span slot="label">${t`Users`}</span> | ||||
|                 </ak-sidebar-item> | ||||
|                 <ak-sidebar-item path="/identity/groups"> | ||||
|                     <span slot="label">${t`Groups`}</span> | ||||
|                 </ak-sidebar-item> | ||||
|                 <ak-sidebar-item | ||||
|                     path="/core/sources" | ||||
|                     .activeWhen=${[`^/core/sources/(?<slug>${SLUG_REGEX})$`]} | ||||
|                 > | ||||
|                     <span slot="label">${t`Federation & Social login`}</span> | ||||
|                 </ak-sidebar-item> | ||||
|                 <ak-sidebar-item path="/core/tokens"> | ||||
|                     <span slot="label">${t`Tokens & App passwords`}</span> | ||||
|                 </ak-sidebar-item> | ||||
|                 <ak-sidebar-item path="/flow/stages/invitations"> | ||||
|                     <span slot="label">${t`Invitations`}</span> | ||||
|                 </ak-sidebar-item> | ||||
|             </ak-sidebar-item> | ||||
|             <ak-sidebar-item> | ||||
|                 <span slot="label">${t`System`}</span> | ||||
|                 <ak-sidebar-item path="/core/tenants"> | ||||
|                     <span slot="label">${t`Tenants`}</span> | ||||
|                 </ak-sidebar-item> | ||||
|                 <ak-sidebar-item path="/crypto/certificates"> | ||||
|                     <span slot="label">${t`Certificates`}</span> | ||||
|                 </ak-sidebar-item> | ||||
|                 <ak-sidebar-item path="/outpost/integrations"> | ||||
|                     <span slot="label">${t`Outpost Integrations`}</span> | ||||
|                 </ak-sidebar-item> | ||||
|             </ak-sidebar-item> | ||||
|         `; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										135
									
								
								web/src/admin/Routes.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										135
									
								
								web/src/admin/Routes.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,135 @@ | ||||
| import "@goauthentik/admin/admin-overview/AdminOverviewPage"; | ||||
| import { ID_REGEX, Route, SLUG_REGEX, UUID_REGEX } from "@goauthentik/elements/router/Route"; | ||||
|  | ||||
| import { html } from "lit"; | ||||
|  | ||||
| export const ROUTES: Route[] = [ | ||||
|     // Prevent infinite Shell loops | ||||
|     new Route(new RegExp("^/$")).redirect("/administration/overview"), | ||||
|     new Route(new RegExp("^#.*")).redirect("/administration/overview"), | ||||
|     new Route(new RegExp("^/library$")).redirect("/if/user/", true), | ||||
|     // statically imported since this is the default route | ||||
|     new Route(new RegExp("^/administration/overview$"), async () => { | ||||
|         return html`<ak-admin-overview></ak-admin-overview>`; | ||||
|     }), | ||||
|     new Route(new RegExp("^/administration/dashboard/users$"), async () => { | ||||
|         await import("@goauthentik/admin/admin-overview/DashboardUserPage"); | ||||
|         return html`<ak-admin-dashboard-users></ak-admin-dashboard-users>`; | ||||
|     }), | ||||
|     new Route(new RegExp("^/administration/system-tasks$"), async () => { | ||||
|         await import("@goauthentik/admin/system-tasks/SystemTaskListPage"); | ||||
|         return html`<ak-system-task-list></ak-system-task-list>`; | ||||
|     }), | ||||
|     new Route(new RegExp("^/core/providers$"), async () => { | ||||
|         await import("@goauthentik/admin/providers/ProviderListPage"); | ||||
|         return html`<ak-provider-list></ak-provider-list>`; | ||||
|     }), | ||||
|     new Route(new RegExp(`^/core/providers/(?<id>${ID_REGEX})$`), async (args) => { | ||||
|         await import("@goauthentik/admin/providers/ProviderViewPage"); | ||||
|         return html`<ak-provider-view .providerID=${parseInt(args.id, 10)}></ak-provider-view>`; | ||||
|     }), | ||||
|     new Route(new RegExp("^/core/applications$"), async () => { | ||||
|         await import("@goauthentik/admin/applications/ApplicationListPage"); | ||||
|         return html`<ak-application-list></ak-application-list>`; | ||||
|     }), | ||||
|     new Route(new RegExp(`^/core/applications/(?<slug>${SLUG_REGEX})$`), async (args) => { | ||||
|         await import("@goauthentik/admin/applications/ApplicationViewPage"); | ||||
|         return html`<ak-application-view .applicationSlug=${args.slug}></ak-application-view>`; | ||||
|     }), | ||||
|     new Route(new RegExp("^/core/sources$"), async () => { | ||||
|         await import("@goauthentik/admin/sources/SourceListPage"); | ||||
|         return html`<ak-source-list></ak-source-list>`; | ||||
|     }), | ||||
|     new Route(new RegExp(`^/core/sources/(?<slug>${SLUG_REGEX})$`), async (args) => { | ||||
|         await import("@goauthentik/admin/sources/SourceViewPage"); | ||||
|         return html`<ak-source-view .sourceSlug=${args.slug}></ak-source-view>`; | ||||
|     }), | ||||
|     new Route(new RegExp("^/core/property-mappings$"), async () => { | ||||
|         await import("@goauthentik/admin/property-mappings/PropertyMappingListPage"); | ||||
|         return html`<ak-property-mapping-list></ak-property-mapping-list>`; | ||||
|     }), | ||||
|     new Route(new RegExp("^/core/tokens$"), async () => { | ||||
|         await import("@goauthentik/admin/tokens/TokenListPage"); | ||||
|         return html`<ak-token-list></ak-token-list>`; | ||||
|     }), | ||||
|     new Route(new RegExp("^/core/tenants$"), async () => { | ||||
|         await import("@goauthentik/admin/tenants/TenantListPage"); | ||||
|         return html`<ak-tenant-list></ak-tenant-list>`; | ||||
|     }), | ||||
|     new Route(new RegExp("^/policy/policies$"), async () => { | ||||
|         await import("@goauthentik/admin/policies/PolicyListPage"); | ||||
|         return html`<ak-policy-list></ak-policy-list>`; | ||||
|     }), | ||||
|     new Route(new RegExp("^/policy/reputation$"), async () => { | ||||
|         await import("@goauthentik/admin/policies/reputation/ReputationListPage"); | ||||
|         return html`<ak-policy-reputation-list></ak-policy-reputation-list>`; | ||||
|     }), | ||||
|     new Route(new RegExp("^/identity/groups$"), async () => { | ||||
|         await import("@goauthentik/admin/groups/GroupListPage"); | ||||
|         return html`<ak-group-list></ak-group-list>`; | ||||
|     }), | ||||
|     new Route(new RegExp(`^/identity/groups/(?<uuid>${UUID_REGEX})$`), async (args) => { | ||||
|         await import("@goauthentik/admin/groups/GroupViewPage"); | ||||
|         return html`<ak-group-view .groupId=${args.uuid}></ak-group-view>`; | ||||
|     }), | ||||
|     new Route(new RegExp("^/identity/users$"), async () => { | ||||
|         await import("@goauthentik/admin/users/UserListPage"); | ||||
|         return html`<ak-user-list></ak-user-list>`; | ||||
|     }), | ||||
|     new Route(new RegExp(`^/identity/users/(?<id>${ID_REGEX})$`), async (args) => { | ||||
|         await import("@goauthentik/admin/users/UserViewPage"); | ||||
|         return html`<ak-user-view .userId=${parseInt(args.id, 10)}></ak-user-view>`; | ||||
|     }), | ||||
|     new Route(new RegExp("^/flow/stages/invitations$"), async () => { | ||||
|         await import("@goauthentik/admin/stages/invitation/InvitationListPage"); | ||||
|         return html`<ak-stage-invitation-list></ak-stage-invitation-list>`; | ||||
|     }), | ||||
|     new Route(new RegExp("^/flow/stages/prompts$"), async () => { | ||||
|         await import("@goauthentik/admin/stages/prompt/PromptListPage"); | ||||
|         return html`<ak-stage-prompt-list></ak-stage-prompt-list>`; | ||||
|     }), | ||||
|     new Route(new RegExp("^/flow/stages$"), async () => { | ||||
|         await import("@goauthentik/admin/stages/StageListPage"); | ||||
|         return html`<ak-stage-list></ak-stage-list>`; | ||||
|     }), | ||||
|     new Route(new RegExp("^/flow/flows$"), async () => { | ||||
|         await import("@goauthentik/admin/flows/FlowListPage"); | ||||
|         return html`<ak-flow-list></ak-flow-list>`; | ||||
|     }), | ||||
|     new Route(new RegExp(`^/flow/flows/(?<slug>${SLUG_REGEX})$`), async (args) => { | ||||
|         await import("@goauthentik/admin/flows/FlowViewPage"); | ||||
|         return html`<ak-flow-view .flowSlug=${args.slug}></ak-flow-view>`; | ||||
|     }), | ||||
|     new Route(new RegExp("^/events/log$"), async () => { | ||||
|         await import("@goauthentik/admin/events/EventListPage"); | ||||
|         return html`<ak-event-list></ak-event-list>`; | ||||
|     }), | ||||
|     new Route(new RegExp(`^/events/log/(?<id>${UUID_REGEX})$`), async (args) => { | ||||
|         await import("@goauthentik/admin/events/EventInfoPage"); | ||||
|         return html`<ak-event-info-page .eventID=${args.id}></ak-event-info-page>`; | ||||
|     }), | ||||
|     new Route(new RegExp("^/events/transports$"), async () => { | ||||
|         await import("@goauthentik/admin/events/TransportListPage"); | ||||
|         return html`<ak-event-transport-list></ak-event-transport-list>`; | ||||
|     }), | ||||
|     new Route(new RegExp("^/events/rules$"), async () => { | ||||
|         await import("@goauthentik/admin/events/RuleListPage"); | ||||
|         return html`<ak-event-rule-list></ak-event-rule-list>`; | ||||
|     }), | ||||
|     new Route(new RegExp("^/outpost/outposts$"), async () => { | ||||
|         await import("@goauthentik/admin/outposts/OutpostListPage"); | ||||
|         return html`<ak-outpost-list></ak-outpost-list>`; | ||||
|     }), | ||||
|     new Route(new RegExp("^/outpost/integrations$"), async () => { | ||||
|         await import("@goauthentik/admin/outposts/ServiceConnectionListPage"); | ||||
|         return html`<ak-outpost-service-connection-list></ak-outpost-service-connection-list>`; | ||||
|     }), | ||||
|     new Route(new RegExp("^/crypto/certificates$"), async () => { | ||||
|         await import("@goauthentik/admin/crypto/CertificateKeyPairListPage"); | ||||
|         return html`<ak-crypto-certificate-list></ak-crypto-certificate-list>`; | ||||
|     }), | ||||
|     new Route(new RegExp("^/blueprints/instances$"), async () => { | ||||
|         await import("@goauthentik/admin/blueprints/BlueprintListPage"); | ||||
|         return html`<ak-blueprint-list></ak-blueprint-list>`; | ||||
|     }), | ||||
| ]; | ||||
							
								
								
									
										223
									
								
								web/src/admin/admin-overview/AdminOverviewPage.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										223
									
								
								web/src/admin/admin-overview/AdminOverviewPage.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,223 @@ | ||||
| import "@goauthentik/admin/admin-overview/TopApplicationsTable"; | ||||
| import "@goauthentik/admin/admin-overview/cards/AdminStatusCard"; | ||||
| import "@goauthentik/admin/admin-overview/cards/SystemStatusCard"; | ||||
| import "@goauthentik/admin/admin-overview/cards/VersionStatusCard"; | ||||
| import "@goauthentik/admin/admin-overview/cards/WorkerStatusCard"; | ||||
| import "@goauthentik/admin/admin-overview/charts/FlowStatusChart"; | ||||
| import "@goauthentik/admin/admin-overview/charts/GroupCountStatusChart"; | ||||
| import "@goauthentik/admin/admin-overview/charts/LDAPSyncStatusChart"; | ||||
| import "@goauthentik/admin/admin-overview/charts/OutpostStatusChart"; | ||||
| import "@goauthentik/admin/admin-overview/charts/PolicyStatusChart"; | ||||
| import "@goauthentik/admin/admin-overview/charts/UserCountStatusChart"; | ||||
| import { me } from "@goauthentik/common/users"; | ||||
| import { AKElement } from "@goauthentik/elements/Base"; | ||||
| import "@goauthentik/elements/PageHeader"; | ||||
| import "@goauthentik/elements/cards/AggregatePromiseCard"; | ||||
| import "@goauthentik/elements/charts/AdminLoginsChart"; | ||||
| import { paramURL } from "@goauthentik/elements/router/RouterOutlet"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { CSSResult, TemplateResult, css, html } from "lit"; | ||||
| import { customElement } from "lit/decorators.js"; | ||||
| import { until } from "lit/directives/until.js"; | ||||
|  | ||||
| import AKGlobal from "@goauthentik/common/styles/authentik.css"; | ||||
| import PFContent from "@patternfly/patternfly/components/Content/content.css"; | ||||
| import PFList from "@patternfly/patternfly/components/List/list.css"; | ||||
| import PFPage from "@patternfly/patternfly/components/Page/page.css"; | ||||
| import PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css"; | ||||
|  | ||||
| @customElement("ak-admin-overview") | ||||
| export class AdminOverviewPage extends AKElement { | ||||
|     static get styles(): CSSResult[] { | ||||
|         return [ | ||||
|             PFGrid, | ||||
|             PFPage, | ||||
|             PFContent, | ||||
|             PFList, | ||||
|             AKGlobal, | ||||
|             css` | ||||
|                 .row-divider { | ||||
|                     margin-top: -4px; | ||||
|                     margin-bottom: -4px; | ||||
|                 } | ||||
|                 .graph-container { | ||||
|                     height: 20em; | ||||
|                 } | ||||
|                 .big-graph-container { | ||||
|                     height: 35em; | ||||
|                 } | ||||
|                 .card-container { | ||||
|                     max-height: 10em; | ||||
|                 } | ||||
|             `, | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     render(): TemplateResult { | ||||
|         return html`<ak-page-header icon="" header="" description=${t`General system status`}> | ||||
|                 <span slot="header"> | ||||
|                     ${until( | ||||
|                         me().then((user) => { | ||||
|                             let name = user.user.username; | ||||
|                             if (user.user.name !== "") { | ||||
|                                 name = user.user.name; | ||||
|                             } | ||||
|                             return t`Welcome, ${name}.`; | ||||
|                         }), | ||||
|                     )} | ||||
|                 </span> | ||||
|             </ak-page-header> | ||||
|             <section class="pf-c-page__main-section"> | ||||
|                 <div class="pf-l-grid pf-m-gutter"> | ||||
|                     <!-- row 1 --> | ||||
|                     <div | ||||
|                         class="pf-l-grid__item pf-m-6-col pf-m-4-col-on-xl pf-m-2-col-on-2xl graph-container" | ||||
|                     > | ||||
|                         <ak-aggregate-card | ||||
|                             icon="fa fa-share" | ||||
|                             header=${t`Quick actions`} | ||||
|                             .isCenter=${false} | ||||
|                         > | ||||
|                             <ul class="pf-c-list"> | ||||
|                                 <li> | ||||
|                                     <a | ||||
|                                         class="pf-u-mb-xl" | ||||
|                                         href=${paramURL("/core/applications", { | ||||
|                                             createForm: true, | ||||
|                                         })} | ||||
|                                         >${t`Create a new application`}</a | ||||
|                                     > | ||||
|                                 </li> | ||||
|                                 <li> | ||||
|                                     <a class="pf-u-mb-xl" href=${paramURL("/events/log")} | ||||
|                                         >${t`Check the logs`}</a | ||||
|                                     > | ||||
|                                 </li> | ||||
|                                 <li> | ||||
|                                     <a | ||||
|                                         class="pf-u-mb-xl" | ||||
|                                         target="_blank" | ||||
|                                         href="https://goauthentik.io/integrations/" | ||||
|                                         >${t`Explore integrations`}</a | ||||
|                                     > | ||||
|                                 </li> | ||||
|                             </ul> | ||||
|                         </ak-aggregate-card> | ||||
|                     </div> | ||||
|                     <div | ||||
|                         class="pf-l-grid__item pf-m-6-col pf-m-4-col-on-xl pf-m-2-col-on-2xl graph-container" | ||||
|                     > | ||||
|                         <ak-aggregate-card | ||||
|                             icon="pf-icon pf-icon-process-automation" | ||||
|                             header=${t`Flows`} | ||||
|                             headerLink="#/flow/flows" | ||||
|                         > | ||||
|                             <ak-admin-status-chart-flow></ak-admin-status-chart-flow> | ||||
|                         </ak-aggregate-card> | ||||
|                     </div> | ||||
|                     <div | ||||
|                         class="pf-l-grid__item pf-m-6-col pf-m-4-col-on-xl pf-m-2-col-on-2xl graph-container" | ||||
|                     > | ||||
|                         <ak-aggregate-card | ||||
|                             icon="pf-icon pf-icon-zone" | ||||
|                             header=${t`Outpost status`} | ||||
|                             headerLink="#/outpost/outposts" | ||||
|                         > | ||||
|                             <ak-admin-status-chart-outpost></ak-admin-status-chart-outpost> | ||||
|                         </ak-aggregate-card> | ||||
|                     </div> | ||||
|                     <div | ||||
|                         class="pf-l-grid__item pf-m-6-col pf-m-4-col-on-xl pf-m-2-col-on-2xl graph-container" | ||||
|                     > | ||||
|                         <ak-aggregate-card | ||||
|                             icon="pf-icon pf-icon-user" | ||||
|                             header=${t`Users`} | ||||
|                             headerLink="#/identity/users" | ||||
|                         > | ||||
|                             <ak-admin-status-chart-user-count></ak-admin-status-chart-user-count> | ||||
|                         </ak-aggregate-card> | ||||
|                     </div> | ||||
|                     <div | ||||
|                         class="pf-l-grid__item pf-m-6-col pf-m-4-col-on-xl pf-m-2-col-on-2xl graph-container" | ||||
|                     > | ||||
|                         <ak-aggregate-card | ||||
|                             icon="pf-icon pf-icon-users" | ||||
|                             header=${t`Groups`} | ||||
|                             headerLink="#/identity/groups" | ||||
|                         > | ||||
|                             <ak-admin-status-chart-group-count></ak-admin-status-chart-group-count> | ||||
|                         </ak-aggregate-card> | ||||
|                     </div> | ||||
|                     <div | ||||
|                         class="pf-l-grid__item pf-m-6-col pf-m-4-col-on-xl pf-m-2-col-on-2xl graph-container" | ||||
|                     > | ||||
|                         <ak-aggregate-card | ||||
|                             icon="fa fa-sync-alt" | ||||
|                             header=${t`LDAP Sync status`} | ||||
|                             headerLink="#/core/sources" | ||||
|                         > | ||||
|                             <ak-admin-status-chart-ldap-sync></ak-admin-status-chart-ldap-sync> | ||||
|                         </ak-aggregate-card> | ||||
|                     </div> | ||||
|                     <div class="pf-l-grid__item pf-m-12-col row-divider"> | ||||
|                         <hr /> | ||||
|                     </div> | ||||
|                     <!-- row 2 --> | ||||
|                     <div | ||||
|                         class="pf-l-grid__item pf-m-6-col pf-m-4-col-on-md pf-m-4-col-on-xl card-container" | ||||
|                     > | ||||
|                         <ak-admin-status-system | ||||
|                             icon="pf-icon pf-icon-server" | ||||
|                             header=${t`System status`} | ||||
|                         > | ||||
|                         </ak-admin-status-system> | ||||
|                     </div> | ||||
|                     <div | ||||
|                         class="pf-l-grid__item pf-m-6-col pf-m-4-col-on-md pf-m-4-col-on-xl card-container" | ||||
|                     > | ||||
|                         <ak-admin-status-version | ||||
|                             icon="pf-icon pf-icon-bundle" | ||||
|                             header=${t`Version`} | ||||
|                             headerLink="https://github.com/goauthentik/authentik/releases" | ||||
|                         > | ||||
|                         </ak-admin-status-version> | ||||
|                     </div> | ||||
|                     <div | ||||
|                         class="pf-l-grid__item pf-m-6-col pf-m-4-col-on-md pf-m-4-col-on-xl card-container" | ||||
|                     > | ||||
|                         <ak-admin-status-card-workers | ||||
|                             icon="pf-icon pf-icon-server" | ||||
|                             header=${t`Workers`} | ||||
|                         > | ||||
|                         </ak-admin-status-card-workers> | ||||
|                     </div> | ||||
|                     <div class="pf-l-grid__item pf-m-12-col row-divider"> | ||||
|                         <hr /> | ||||
|                     </div> | ||||
|                     <!-- row 3 --> | ||||
|                     <div | ||||
|                         class="pf-l-grid__item pf-m-12-col pf-m-6-col-on-xl pf-m-8-col-on-2xl big-graph-container" | ||||
|                     > | ||||
|                         <ak-aggregate-card | ||||
|                             icon="pf-icon pf-icon-server" | ||||
|                             header=${t`Logins over the last 24 hours`} | ||||
|                         > | ||||
|                             <ak-charts-admin-login></ak-charts-admin-login> | ||||
|                         </ak-aggregate-card> | ||||
|                     </div> | ||||
|                     <div | ||||
|                         class="pf-l-grid__item pf-m-12-col pf-m-6-col-on-xl pf-m-4-col-on-2xl big-graph-container" | ||||
|                     > | ||||
|                         <ak-aggregate-card | ||||
|                             icon="pf-icon pf-icon-server" | ||||
|                             header=${t`Apps with most usage`} | ||||
|                         > | ||||
|                             <ak-top-applications-table></ak-top-applications-table> | ||||
|                         </ak-aggregate-card> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </section>`; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										87
									
								
								web/src/admin/admin-overview/DashboardUserPage.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								web/src/admin/admin-overview/DashboardUserPage.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,87 @@ | ||||
| import { AKElement } from "@goauthentik/elements/Base"; | ||||
| import "@goauthentik/elements/PageHeader"; | ||||
| import "@goauthentik/elements/cards/AggregatePromiseCard"; | ||||
| import "@goauthentik/elements/charts/AdminModelPerDay"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { CSSResult, TemplateResult, css, html } from "lit"; | ||||
| import { customElement } from "lit/decorators.js"; | ||||
|  | ||||
| import AKGlobal from "@goauthentik/common/styles/authentik.css"; | ||||
| import PFContent from "@patternfly/patternfly/components/Content/content.css"; | ||||
| import PFList from "@patternfly/patternfly/components/List/list.css"; | ||||
| import PFPage from "@patternfly/patternfly/components/Page/page.css"; | ||||
| import PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css"; | ||||
|  | ||||
| import { EventActions } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-admin-dashboard-users") | ||||
| export class DashboardUserPage extends AKElement { | ||||
|     static get styles(): CSSResult[] { | ||||
|         return [ | ||||
|             PFGrid, | ||||
|             PFPage, | ||||
|             PFContent, | ||||
|             PFList, | ||||
|             AKGlobal, | ||||
|             css` | ||||
|                 .row-divider { | ||||
|                     margin-top: -4px; | ||||
|                     margin-bottom: -4px; | ||||
|                 } | ||||
|                 .graph-container { | ||||
|                     height: 20em; | ||||
|                 } | ||||
|                 .big-graph-container { | ||||
|                     height: 35em; | ||||
|                 } | ||||
|                 .card-container { | ||||
|                     max-height: 10em; | ||||
|                 } | ||||
|             `, | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     render(): TemplateResult { | ||||
|         return html`<ak-page-header icon="pf-icon pf-icon-user" header=${t`User statistics`}> | ||||
|             </ak-page-header> | ||||
|             <section class="pf-c-page__main-section"> | ||||
|                 <div class="pf-l-grid pf-m-gutter"> | ||||
|                     <div | ||||
|                         class="pf-l-grid__item pf-m-12-col pf-m-12-col-on-xl pf-m-12-col-on-2xl big-graph-container" | ||||
|                     > | ||||
|                         <ak-aggregate-card header=${t`Users created per day in the last month`}> | ||||
|                             <ak-charts-admin-model-per-day | ||||
|                                 .query=${{ | ||||
|                                     context__model__app: "authentik_core", | ||||
|                                     context__model__model_name: "user", | ||||
|                                 }} | ||||
|                             > | ||||
|                             </ak-charts-admin-model-per-day> | ||||
|                         </ak-aggregate-card> | ||||
|                     </div> | ||||
|                     <div class="pf-l-grid__item pf-m-12-col row-divider"> | ||||
|                         <hr /> | ||||
|                     </div> | ||||
|                     <!-- row 2 --> | ||||
|                     <div | ||||
|                         class="pf-l-grid__item pf-m-12-col pf-m-6-col-on-xl pf-m-6-col-on-2xl big-graph-container" | ||||
|                     > | ||||
|                         <ak-aggregate-card header=${t`Logins per day in the last month`}> | ||||
|                             <ak-charts-admin-model-per-day action=${EventActions.Login}> | ||||
|                             </ak-charts-admin-model-per-day> | ||||
|                         </ak-aggregate-card> | ||||
|                     </div> | ||||
|                     <div | ||||
|                         class="pf-l-grid__item pf-m-12-col pf-m-6-col-on-xl pf-m-6-col-on-2xl big-graph-container" | ||||
|                     > | ||||
|                         <ak-aggregate-card header=${t`Failed Logins per day in the last month`}> | ||||
|                             <ak-charts-admin-model-per-day action=${EventActions.LoginFailed}> | ||||
|                             </ak-charts-admin-model-per-day> | ||||
|                         </ak-aggregate-card> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </section> `; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										64
									
								
								web/src/admin/admin-overview/TopApplicationsTable.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								web/src/admin/admin-overview/TopApplicationsTable.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,64 @@ | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { AKElement } from "@goauthentik/elements/Base"; | ||||
| import "@goauthentik/elements/Spinner"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { CSSResult, TemplateResult, html } from "lit"; | ||||
| import { customElement, property } from "lit/decorators.js"; | ||||
|  | ||||
| import AKGlobal from "@goauthentik/common/styles/authentik.css"; | ||||
| import PFTable from "@patternfly/patternfly/components/Table/table.css"; | ||||
|  | ||||
| import { EventTopPerUser, EventsApi } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-top-applications-table") | ||||
| export class TopApplicationsTable extends AKElement { | ||||
|     @property({ attribute: false }) | ||||
|     topN?: EventTopPerUser[]; | ||||
|  | ||||
|     static get styles(): CSSResult[] { | ||||
|         return [PFTable, AKGlobal]; | ||||
|     } | ||||
|  | ||||
|     firstUpdated(): void { | ||||
|         new EventsApi(DEFAULT_CONFIG) | ||||
|             .eventsEventsTopPerUserList({ | ||||
|                 action: "authorize_application", | ||||
|                 topN: 11, | ||||
|             }) | ||||
|             .then((events) => { | ||||
|                 this.topN = events; | ||||
|             }); | ||||
|     } | ||||
|  | ||||
|     renderRow(event: EventTopPerUser): TemplateResult { | ||||
|         return html`<tr role="row"> | ||||
|             <td role="cell">${event.application.name}</td> | ||||
|             <td role="cell">${event.countedEvents}</td> | ||||
|             <td role="cell"> | ||||
|                 <progress | ||||
|                     value="${event.countedEvents}" | ||||
|                     max="${this.topN ? this.topN[0].countedEvents : 0}" | ||||
|                 ></progress> | ||||
|             </td> | ||||
|         </tr>`; | ||||
|     } | ||||
|  | ||||
|     render(): TemplateResult { | ||||
|         return html`<table class="pf-c-table pf-m-compact" role="grid"> | ||||
|             <thead> | ||||
|                 <tr role="row"> | ||||
|                     <th role="columnheader" scope="col">${t`Application`}</th> | ||||
|                     <th role="columnheader" scope="col">${t`Logins`}</th> | ||||
|                     <th role="columnheader" scope="col"></th> | ||||
|                 </tr> | ||||
|             </thead> | ||||
|             <tbody role="rowgroup"> | ||||
|                 ${this.topN | ||||
|                     ? this.topN.map((e) => this.renderRow(e)) | ||||
|                     : html`<ak-spinner></ak-spinner>`} | ||||
|             </tbody> | ||||
|         </table>`; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										49
									
								
								web/src/admin/admin-overview/cards/AdminStatusCard.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								web/src/admin/admin-overview/cards/AdminStatusCard.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,49 @@ | ||||
| import { EVENT_REFRESH } from "@goauthentik/common/constants"; | ||||
| import { PFSize } from "@goauthentik/elements/Spinner"; | ||||
| import { AggregateCard } from "@goauthentik/elements/cards/AggregateCard"; | ||||
|  | ||||
| import { TemplateResult, html } from "lit"; | ||||
| import { until } from "lit/directives/until.js"; | ||||
|  | ||||
| export interface AdminStatus { | ||||
|     icon: string; | ||||
|     message?: TemplateResult; | ||||
| } | ||||
|  | ||||
| export abstract class AdminStatusCard<T> extends AggregateCard { | ||||
|     abstract getPrimaryValue(): Promise<T>; | ||||
|  | ||||
|     abstract getStatus(value: T): Promise<AdminStatus>; | ||||
|  | ||||
|     value?: T; | ||||
|  | ||||
|     constructor() { | ||||
|         super(); | ||||
|         this.addEventListener(EVENT_REFRESH, () => { | ||||
|             this.requestUpdate(); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     renderValue(): TemplateResult { | ||||
|         return html`${this.value}`; | ||||
|     } | ||||
|  | ||||
|     renderInner(): TemplateResult { | ||||
|         return html`<p class="center-value"> | ||||
|             ${until( | ||||
|                 this.getPrimaryValue() | ||||
|                     .then((v) => { | ||||
|                         this.value = v; | ||||
|                         return this.getStatus(v); | ||||
|                     }) | ||||
|                     .then((status) => { | ||||
|                         return html`<p><i class="${status.icon}"></i> ${this.renderValue()}</p> | ||||
|                             ${status.message | ||||
|                                 ? html`<p class="subtext">${status.message}</p>` | ||||
|                                 : html``}`; | ||||
|                     }), | ||||
|                 html`<ak-spinner size="${PFSize.Large}"></ak-spinner>`, | ||||
|             )} | ||||
|         </p>`; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										83
									
								
								web/src/admin/admin-overview/cards/SystemStatusCard.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								web/src/admin/admin-overview/cards/SystemStatusCard.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,83 @@ | ||||
| import { | ||||
|     AdminStatus, | ||||
|     AdminStatusCard, | ||||
| } from "@goauthentik/admin/admin-overview/cards/AdminStatusCard"; | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { TemplateResult, html } from "lit"; | ||||
| import { customElement } from "lit/decorators.js"; | ||||
|  | ||||
| import { AdminApi, OutpostsApi, System } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-admin-status-system") | ||||
| export class SystemStatusCard extends AdminStatusCard<System> { | ||||
|     now?: Date; | ||||
|  | ||||
|     header = t`OK`; | ||||
|  | ||||
|     async getPrimaryValue(): Promise<System> { | ||||
|         this.now = new Date(); | ||||
|         let status = await new AdminApi(DEFAULT_CONFIG).adminSystemRetrieve(); | ||||
|         if (status.embeddedOutpostHost === "" || !status.embeddedOutpostHost.includes("http")) { | ||||
|             // First install, ensure the embedded outpost host is set | ||||
|             // also run when outpost host does not contain http | ||||
|             // (yes it's called host and requires a URL, i know) | ||||
|             await this.setOutpostHost(); | ||||
|             status = await new AdminApi(DEFAULT_CONFIG).adminSystemRetrieve(); | ||||
|         } | ||||
|         return status; | ||||
|     } | ||||
|  | ||||
|     // Called on fresh installations and whenever the embedded outpost is deleted | ||||
|     // automatically send the login URL when the user first visits the admin dashboard. | ||||
|     async setOutpostHost(): Promise<void> { | ||||
|         const outposts = await new OutpostsApi(DEFAULT_CONFIG).outpostsInstancesList({ | ||||
|             managedIexact: "goauthentik.io/outposts/embedded", | ||||
|         }); | ||||
|         if (outposts.results.length < 1) { | ||||
|             return; | ||||
|         } | ||||
|         const outpost = outposts.results[0]; | ||||
|         outpost.config["authentik_host"] = window.location.origin; | ||||
|         await new OutpostsApi(DEFAULT_CONFIG).outpostsInstancesUpdate({ | ||||
|             uuid: outpost.pk, | ||||
|             outpostRequest: outpost, | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     getStatus(value: System): Promise<AdminStatus> { | ||||
|         if (value.embeddedOutpostHost === "") { | ||||
|             this.header = t`Warning`; | ||||
|             return Promise.resolve<AdminStatus>({ | ||||
|                 icon: "fa fa-exclamation-triangle pf-m-warning", | ||||
|                 message: html`${t`Embedded outpost is not configured correctly.`} | ||||
|                     <a href="#/outpost/outposts">${t`Check outposts.`}</a>`, | ||||
|             }); | ||||
|         } | ||||
|         if (!value.httpIsSecure && document.location.protocol === "https:") { | ||||
|             this.header = t`Warning`; | ||||
|             return Promise.resolve<AdminStatus>({ | ||||
|                 icon: "fa fa-exclamation-triangle pf-m-warning", | ||||
|                 message: html`${t`HTTPS is not detected correctly`}`, | ||||
|             }); | ||||
|         } | ||||
|         const timeDiff = value.serverTime.getTime() - (this.now || new Date()).getTime(); | ||||
|         if (timeDiff > 5000 || timeDiff < -5000) { | ||||
|             this.header = t`Warning`; | ||||
|             return Promise.resolve<AdminStatus>({ | ||||
|                 icon: "fa fa-exclamation-triangle pf-m-warning", | ||||
|                 message: html`${t`Server and client are further than 5 seconds apart.`}`, | ||||
|             }); | ||||
|         } | ||||
|         return Promise.resolve<AdminStatus>({ | ||||
|             icon: "fa fa-check-circle pf-m-success", | ||||
|             message: html`${t`Everything is ok.`}`, | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     renderValue(): TemplateResult { | ||||
|         return html`${this.header}`; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										50
									
								
								web/src/admin/admin-overview/cards/VersionStatusCard.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								web/src/admin/admin-overview/cards/VersionStatusCard.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,50 @@ | ||||
| import { | ||||
|     AdminStatus, | ||||
|     AdminStatusCard, | ||||
| } from "@goauthentik/admin/admin-overview/cards/AdminStatusCard"; | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { TemplateResult, html } from "lit"; | ||||
| import { customElement } from "lit/decorators.js"; | ||||
|  | ||||
| import { AdminApi, Version } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-admin-status-version") | ||||
| export class VersionStatusCard extends AdminStatusCard<Version> { | ||||
|     getPrimaryValue(): Promise<Version> { | ||||
|         return new AdminApi(DEFAULT_CONFIG).adminVersionRetrieve(); | ||||
|     } | ||||
|  | ||||
|     getStatus(value: Version): Promise<AdminStatus> { | ||||
|         if (value.buildHash) { | ||||
|             return Promise.resolve<AdminStatus>({ | ||||
|                 icon: "fa fa-check-circle pf-m-success", | ||||
|                 message: html` | ||||
|                     ${t`Build hash: `} | ||||
|                     <a | ||||
|                         href="https://github.com/goauthentik/authentik/commit/${value.buildHash}" | ||||
|                         target="_blank" | ||||
|                     > | ||||
|                         ${value.buildHash?.substring(0, 7)} | ||||
|                     </a> | ||||
|                 `, | ||||
|             }); | ||||
|         } | ||||
|         if (value.outdated) { | ||||
|             return Promise.resolve<AdminStatus>({ | ||||
|                 icon: "fa fa-exclamation-triangle pf-m-warning", | ||||
|                 message: html`${t`${value.versionLatest} is available!`}`, | ||||
|             }); | ||||
|         } | ||||
|         return Promise.resolve<AdminStatus>({ | ||||
|             icon: "fa fa-check-circle pf-m-success", | ||||
|             message: html`${t`Up-to-date!`}`, | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     renderValue(): TemplateResult { | ||||
|         return html`${this.value?.versionCurrent}`; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										34
									
								
								web/src/admin/admin-overview/cards/WorkerStatusCard.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								web/src/admin/admin-overview/cards/WorkerStatusCard.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,34 @@ | ||||
| import { | ||||
|     AdminStatus, | ||||
|     AdminStatusCard, | ||||
| } from "@goauthentik/admin/admin-overview/cards/AdminStatusCard"; | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { html } from "lit"; | ||||
| import { customElement } from "lit/decorators.js"; | ||||
|  | ||||
| import { AdminApi } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-admin-status-card-workers") | ||||
| export class WorkersStatusCard extends AdminStatusCard<number> { | ||||
|     getPrimaryValue(): Promise<number> { | ||||
|         return new AdminApi(DEFAULT_CONFIG).adminWorkersRetrieve().then((workers) => { | ||||
|             return workers.count; | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     getStatus(value: number): Promise<AdminStatus> { | ||||
|         if (value < 1) { | ||||
|             return Promise.resolve<AdminStatus>({ | ||||
|                 icon: "fa fa-times-circle pf-m-danger", | ||||
|                 message: html`${t`No workers connected. Background tasks will not run.`}`, | ||||
|             }); | ||||
|         } else { | ||||
|             return Promise.resolve<AdminStatus>({ | ||||
|                 icon: "fa fa-check-circle pf-m-success", | ||||
|             }); | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										61
									
								
								web/src/admin/admin-overview/charts/FlowStatusChart.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								web/src/admin/admin-overview/charts/FlowStatusChart.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,61 @@ | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { AKChart } from "@goauthentik/elements/charts/Chart"; | ||||
| import "@goauthentik/elements/forms/ConfirmationForm"; | ||||
| import { ChartData, ChartOptions } from "chart.js"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { customElement } from "lit/decorators.js"; | ||||
|  | ||||
| import { FlowsApi } from "@goauthentik/api"; | ||||
|  | ||||
| interface FlowMetrics { | ||||
|     count: number; | ||||
|     cached: number; | ||||
| } | ||||
|  | ||||
| @customElement("ak-admin-status-chart-flow") | ||||
| export class PolicyStatusChart extends AKChart<FlowMetrics> { | ||||
|     getChartType(): string { | ||||
|         return "doughnut"; | ||||
|     } | ||||
|  | ||||
|     getOptions(): ChartOptions { | ||||
|         return { | ||||
|             plugins: { | ||||
|                 legend: { | ||||
|                     display: false, | ||||
|                 }, | ||||
|             }, | ||||
|             maintainAspectRatio: false, | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     async apiRequest(): Promise<FlowMetrics> { | ||||
|         const api = new FlowsApi(DEFAULT_CONFIG); | ||||
|         const cached = (await api.flowsInstancesCacheInfoRetrieve()).count || 0; | ||||
|         const count = ( | ||||
|             await api.flowsInstancesList({ | ||||
|                 pageSize: 1, | ||||
|             }) | ||||
|         ).pagination.count; | ||||
|         this.centerText = count.toString(); | ||||
|         return { | ||||
|             count: count - cached, | ||||
|             cached: cached, | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     getChartData(data: FlowMetrics): ChartData { | ||||
|         return { | ||||
|             labels: [t`Total flows`, t`Cached flows`], | ||||
|             datasets: [ | ||||
|                 { | ||||
|                     backgroundColor: ["#2b9af3", "#3e8635"], | ||||
|                     spanGaps: true, | ||||
|                     data: [data.count, data.cached], | ||||
|                 }, | ||||
|             ], | ||||
|         }; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										64
									
								
								web/src/admin/admin-overview/charts/GroupCountStatusChart.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								web/src/admin/admin-overview/charts/GroupCountStatusChart.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,64 @@ | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { AKChart } from "@goauthentik/elements/charts/Chart"; | ||||
| import { ChartData, ChartOptions } from "chart.js"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { customElement } from "lit/decorators.js"; | ||||
|  | ||||
| import { CoreApi } from "@goauthentik/api"; | ||||
|  | ||||
| interface GroupMetrics { | ||||
|     count: number; | ||||
|     superusers: number; | ||||
| } | ||||
|  | ||||
| @customElement("ak-admin-status-chart-group-count") | ||||
| export class GroupCountStatusChart extends AKChart<GroupMetrics> { | ||||
|     getChartType(): string { | ||||
|         return "doughnut"; | ||||
|     } | ||||
|  | ||||
|     getOptions(): ChartOptions { | ||||
|         return { | ||||
|             plugins: { | ||||
|                 legend: { | ||||
|                     display: false, | ||||
|                 }, | ||||
|             }, | ||||
|             maintainAspectRatio: false, | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     async apiRequest(): Promise<GroupMetrics> { | ||||
|         const api = new CoreApi(DEFAULT_CONFIG); | ||||
|         const count = ( | ||||
|             await api.coreGroupsList({ | ||||
|                 pageSize: 1, | ||||
|             }) | ||||
|         ).pagination.count; | ||||
|         const superusers = ( | ||||
|             await api.coreGroupsList({ | ||||
|                 isSuperuser: true, | ||||
|             }) | ||||
|         ).pagination.count; | ||||
|         this.centerText = count.toString(); | ||||
|         return { | ||||
|             count: count - superusers, | ||||
|             superusers, | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     getChartData(data: GroupMetrics): ChartData { | ||||
|         return { | ||||
|             labels: [t`Total groups`, t`Superuser-groups`], | ||||
|             datasets: [ | ||||
|                 { | ||||
|                     backgroundColor: ["#2b9af3", "#3e8635"], | ||||
|                     spanGaps: true, | ||||
|                     data: [data.count, data.superusers], | ||||
|                 }, | ||||
|             ], | ||||
|         }; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										88
									
								
								web/src/admin/admin-overview/charts/LDAPSyncStatusChart.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								web/src/admin/admin-overview/charts/LDAPSyncStatusChart.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,88 @@ | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { AKChart } from "@goauthentik/elements/charts/Chart"; | ||||
| import "@goauthentik/elements/forms/ConfirmationForm"; | ||||
| import { ChartData, ChartOptions } from "chart.js"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { customElement } from "lit/decorators.js"; | ||||
|  | ||||
| import { SourcesApi, TaskStatusEnum } from "@goauthentik/api"; | ||||
|  | ||||
| interface LDAPSyncStats { | ||||
|     healthy: number; | ||||
|     failed: number; | ||||
|     unsynced: number; | ||||
| } | ||||
|  | ||||
| @customElement("ak-admin-status-chart-ldap-sync") | ||||
| export class LDAPSyncStatusChart extends AKChart<LDAPSyncStats> { | ||||
|     getChartType(): string { | ||||
|         return "doughnut"; | ||||
|     } | ||||
|  | ||||
|     getOptions(): ChartOptions { | ||||
|         return { | ||||
|             plugins: { | ||||
|                 legend: { | ||||
|                     display: false, | ||||
|                 }, | ||||
|             }, | ||||
|             maintainAspectRatio: false, | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     async apiRequest(): Promise<LDAPSyncStats> { | ||||
|         const api = new SourcesApi(DEFAULT_CONFIG); | ||||
|         const sources = await api.sourcesLdapList({}); | ||||
|         const metrics: { [key: string]: number } = { | ||||
|             healthy: 0, | ||||
|             failed: 0, | ||||
|             unsynced: 0, | ||||
|         }; | ||||
|         await Promise.all( | ||||
|             sources.results.map(async (element) => { | ||||
|                 // Each source should have 3 successful tasks, so the worst task overwrites | ||||
|                 let sourceKey = "healthy"; | ||||
|                 try { | ||||
|                     const health = await api.sourcesLdapSyncStatusList({ | ||||
|                         slug: element.slug, | ||||
|                     }); | ||||
|  | ||||
|                     health.forEach((task) => { | ||||
|                         if (task.status !== TaskStatusEnum.Successful) { | ||||
|                             sourceKey = "failed"; | ||||
|                         } | ||||
|                         const now = new Date().getTime(); | ||||
|                         const maxDelta = 3600000; // 1 hour | ||||
|                         if (!health || now - task.taskFinishTimestamp.getTime() > maxDelta) { | ||||
|                             sourceKey = "unsynced"; | ||||
|                         } | ||||
|                     }); | ||||
|                 } catch { | ||||
|                     sourceKey = "unsynced"; | ||||
|                 } | ||||
|                 metrics[sourceKey] += 1; | ||||
|             }), | ||||
|         ); | ||||
|         this.centerText = sources.pagination.count.toString(); | ||||
|         return { | ||||
|             healthy: sources.pagination.count === 0 ? -1 : metrics.healthy, | ||||
|             failed: metrics.failed, | ||||
|             unsynced: metrics.unsynced, | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     getChartData(data: LDAPSyncStats): ChartData { | ||||
|         return { | ||||
|             labels: [t`Healthy sources`, t`Failed sources`, t`Unsynced sources`], | ||||
|             datasets: [ | ||||
|                 { | ||||
|                     backgroundColor: ["#3e8635", "#C9190B", "#2b9af3"], | ||||
|                     spanGaps: true, | ||||
|                     data: [data.healthy, data.failed, data.unsynced], | ||||
|                 }, | ||||
|             ], | ||||
|         }; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										78
									
								
								web/src/admin/admin-overview/charts/OutpostStatusChart.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								web/src/admin/admin-overview/charts/OutpostStatusChart.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,78 @@ | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { AKChart } from "@goauthentik/elements/charts/Chart"; | ||||
| import "@goauthentik/elements/forms/ConfirmationForm"; | ||||
| import { ChartData, ChartOptions } from "chart.js"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { customElement } from "lit/decorators.js"; | ||||
|  | ||||
| import { OutpostsApi } from "@goauthentik/api"; | ||||
|  | ||||
| interface OutpostStats { | ||||
|     healthy: number; | ||||
|     outdated: number; | ||||
|     unhealthy: number; | ||||
| } | ||||
|  | ||||
| @customElement("ak-admin-status-chart-outpost") | ||||
| export class OutpostStatusChart extends AKChart<OutpostStats> { | ||||
|     getChartType(): string { | ||||
|         return "doughnut"; | ||||
|     } | ||||
|  | ||||
|     getOptions(): ChartOptions { | ||||
|         return { | ||||
|             plugins: { | ||||
|                 legend: { | ||||
|                     display: false, | ||||
|                 }, | ||||
|             }, | ||||
|             maintainAspectRatio: false, | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     async apiRequest(): Promise<OutpostStats> { | ||||
|         const api = new OutpostsApi(DEFAULT_CONFIG); | ||||
|         const outposts = await api.outpostsInstancesList({}); | ||||
|         let healthy = 0; | ||||
|         let outdated = 0; | ||||
|         let unhealthy = 0; | ||||
|         await Promise.all( | ||||
|             outposts.results.map(async (element) => { | ||||
|                 const health = await api.outpostsInstancesHealthList({ | ||||
|                     uuid: element.pk || "", | ||||
|                 }); | ||||
|                 if (health.length === 0) { | ||||
|                     unhealthy += 1; | ||||
|                 } | ||||
|                 health.forEach((h) => { | ||||
|                     if (h.versionOutdated) { | ||||
|                         outdated += 1; | ||||
|                     } else { | ||||
|                         healthy += 1; | ||||
|                     } | ||||
|                 }); | ||||
|             }), | ||||
|         ); | ||||
|         this.centerText = outposts.pagination.count.toString(); | ||||
|         return { | ||||
|             healthy: outposts.pagination.count === 0 ? -1 : healthy, | ||||
|             outdated, | ||||
|             unhealthy, | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     getChartData(data: OutpostStats): ChartData { | ||||
|         return { | ||||
|             labels: [t`Healthy outposts`, t`Outdated outposts`, t`Unhealthy outposts`], | ||||
|             datasets: [ | ||||
|                 { | ||||
|                     backgroundColor: ["#3e8635", "#f0ab00", "#C9190B"], | ||||
|                     spanGaps: true, | ||||
|                     data: [data.healthy, data.outdated, data.unhealthy], | ||||
|                 }, | ||||
|             ], | ||||
|         }; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										71
									
								
								web/src/admin/admin-overview/charts/PolicyStatusChart.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								web/src/admin/admin-overview/charts/PolicyStatusChart.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,71 @@ | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { AKChart } from "@goauthentik/elements/charts/Chart"; | ||||
| import "@goauthentik/elements/forms/ConfirmationForm"; | ||||
| import { ChartData, ChartOptions } from "chart.js"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { customElement } from "lit/decorators.js"; | ||||
|  | ||||
| import { PoliciesApi } from "@goauthentik/api"; | ||||
|  | ||||
| interface PolicyMetrics { | ||||
|     count: number; | ||||
|     cached: number; | ||||
|     unbound: number; | ||||
| } | ||||
|  | ||||
| @customElement("ak-admin-status-chart-policy") | ||||
| export class PolicyStatusChart extends AKChart<PolicyMetrics> { | ||||
|     getChartType(): string { | ||||
|         return "doughnut"; | ||||
|     } | ||||
|  | ||||
|     getOptions(): ChartOptions { | ||||
|         return { | ||||
|             plugins: { | ||||
|                 legend: { | ||||
|                     display: false, | ||||
|                 }, | ||||
|             }, | ||||
|             maintainAspectRatio: false, | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     async apiRequest(): Promise<PolicyMetrics> { | ||||
|         const api = new PoliciesApi(DEFAULT_CONFIG); | ||||
|         const cached = (await api.policiesAllCacheInfoRetrieve()).count || 0; | ||||
|         const count = ( | ||||
|             await api.policiesAllList({ | ||||
|                 pageSize: 1, | ||||
|             }) | ||||
|         ).pagination.count; | ||||
|         const unbound = ( | ||||
|             await api.policiesAllList({ | ||||
|                 bindingsIsnull: true, | ||||
|                 promptstageIsnull: true, | ||||
|             }) | ||||
|         ).pagination.count; | ||||
|         this.centerText = count.toString(); | ||||
|         return { | ||||
|             // If we have more cache than total policies, only show that | ||||
|             // otherwise show count without unbound | ||||
|             count: cached >= count ? cached : count - unbound, | ||||
|             cached: cached, | ||||
|             unbound: unbound, | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     getChartData(data: PolicyMetrics): ChartData { | ||||
|         return { | ||||
|             labels: [t`Total policies`, t`Cached policies`, t`Unbound policies`], | ||||
|             datasets: [ | ||||
|                 { | ||||
|                     backgroundColor: ["#2b9af3", "#3e8635", "#f0ab00"], | ||||
|                     spanGaps: true, | ||||
|                     data: [data.count, data.cached, data.unbound], | ||||
|                 }, | ||||
|             ], | ||||
|         }; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										64
									
								
								web/src/admin/admin-overview/charts/UserCountStatusChart.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								web/src/admin/admin-overview/charts/UserCountStatusChart.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,64 @@ | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { AKChart } from "@goauthentik/elements/charts/Chart"; | ||||
| import { ChartData, ChartOptions } from "chart.js"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { customElement } from "lit/decorators.js"; | ||||
|  | ||||
| import { CoreApi } from "@goauthentik/api"; | ||||
|  | ||||
| interface UserMetrics { | ||||
|     count: number; | ||||
|     superusers: number; | ||||
| } | ||||
|  | ||||
| @customElement("ak-admin-status-chart-user-count") | ||||
| export class UserCountStatusChart extends AKChart<UserMetrics> { | ||||
|     getChartType(): string { | ||||
|         return "doughnut"; | ||||
|     } | ||||
|  | ||||
|     getOptions(): ChartOptions { | ||||
|         return { | ||||
|             plugins: { | ||||
|                 legend: { | ||||
|                     display: false, | ||||
|                 }, | ||||
|             }, | ||||
|             maintainAspectRatio: false, | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     async apiRequest(): Promise<UserMetrics> { | ||||
|         const api = new CoreApi(DEFAULT_CONFIG); | ||||
|         const count = ( | ||||
|             await api.coreUsersList({ | ||||
|                 pageSize: 1, | ||||
|             }) | ||||
|         ).pagination.count; | ||||
|         const superusers = ( | ||||
|             await api.coreUsersList({ | ||||
|                 isSuperuser: true, | ||||
|             }) | ||||
|         ).pagination.count; | ||||
|         this.centerText = count.toString(); | ||||
|         return { | ||||
|             count: count - superusers, | ||||
|             superusers, | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     getChartData(data: UserMetrics): ChartData { | ||||
|         return { | ||||
|             labels: [t`Total users`, t`Superusers`], | ||||
|             datasets: [ | ||||
|                 { | ||||
|                     backgroundColor: ["#2b9af3", "#3e8635"], | ||||
|                     spanGaps: true, | ||||
|                     data: [data.count, data.superusers], | ||||
|                 }, | ||||
|             ], | ||||
|         }; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										140
									
								
								web/src/admin/applications/ApplicationCheckAccessForm.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										140
									
								
								web/src/admin/applications/ApplicationCheckAccessForm.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,140 @@ | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { PFColor } from "@goauthentik/elements/Label"; | ||||
| import { Form } from "@goauthentik/elements/forms/Form"; | ||||
| import "@goauthentik/elements/forms/HorizontalFormElement"; | ||||
| import { UserOption } from "@goauthentik/elements/user/utils"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { CSSResult, TemplateResult, html } from "lit"; | ||||
| import { customElement, property } from "lit/decorators.js"; | ||||
| import { until } from "lit/directives/until.js"; | ||||
|  | ||||
| import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css"; | ||||
|  | ||||
| import { Application, CoreApi, PolicyTestResult } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-application-check-access-form") | ||||
| export class ApplicationCheckAccessForm extends Form<{ forUser: number }> { | ||||
|     @property({ attribute: false }) | ||||
|     application!: Application; | ||||
|  | ||||
|     @property({ attribute: false }) | ||||
|     result?: PolicyTestResult; | ||||
|  | ||||
|     @property({ attribute: false }) | ||||
|     request?: number; | ||||
|  | ||||
|     getSuccessMessage(): string { | ||||
|         return t`Successfully sent test-request.`; | ||||
|     } | ||||
|  | ||||
|     send = (data: { forUser: number }): Promise<PolicyTestResult> => { | ||||
|         this.request = data.forUser; | ||||
|         return new CoreApi(DEFAULT_CONFIG) | ||||
|             .coreApplicationsCheckAccessRetrieve({ | ||||
|                 slug: this.application?.slug, | ||||
|                 forUser: data.forUser, | ||||
|             }) | ||||
|             .then((result) => (this.result = result)); | ||||
|     }; | ||||
|  | ||||
|     resetForm(): void { | ||||
|         super.resetForm(); | ||||
|         this.result = undefined; | ||||
|     } | ||||
|  | ||||
|     static get styles(): CSSResult[] { | ||||
|         return super.styles.concat(PFDescriptionList); | ||||
|     } | ||||
|  | ||||
|     renderResult(): TemplateResult { | ||||
|         return html` | ||||
|             <ak-form-element-horizontal label=${t`Passing`}> | ||||
|                 <div class="pf-c-form__group-label"> | ||||
|                     <div class="c-form__horizontal-group"> | ||||
|                         <span class="pf-c-form__label-text"> | ||||
|                             <ak-label color=${this.result?.passing ? PFColor.Green : PFColor.Red}> | ||||
|                                 ${this.result?.passing ? t`Yes` : t`No`} | ||||
|                             </ak-label> | ||||
|                         </span> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-element-horizontal label=${t`Messages`}> | ||||
|                 <div class="pf-c-form__group-label"> | ||||
|                     <div class="c-form__horizontal-group"> | ||||
|                         <ul> | ||||
|                             ${(this.result?.messages || []).length > 0 | ||||
|                                 ? this.result?.messages?.map((m) => { | ||||
|                                       return html`<li> | ||||
|                                           <span class="pf-c-form__label-text">${m}</span> | ||||
|                                       </li>`; | ||||
|                                   }) | ||||
|                                 : html`<li> | ||||
|                                       <span class="pf-c-form__label-text">-</span> | ||||
|                                   </li>`} | ||||
|                         </ul> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-element-horizontal label=${t`Log messages`}> | ||||
|                 <div class="pf-c-form__group-label"> | ||||
|                     <div class="c-form__horizontal-group"> | ||||
|                         <dl class="pf-c-description-list pf-m-horizontal"> | ||||
|                             ${(this.result?.logMessages || []).length > 0 | ||||
|                                 ? this.result?.logMessages?.map((m) => { | ||||
|                                       return html`<div class="pf-c-description-list__group"> | ||||
|                                           <dt class="pf-c-description-list__term"> | ||||
|                                               <span class="pf-c-description-list__text" | ||||
|                                                   >${m.log_level}</span | ||||
|                                               > | ||||
|                                           </dt> | ||||
|                                           <dd class="pf-c-description-list__description"> | ||||
|                                               <div class="pf-c-description-list__text"> | ||||
|                                                   ${m.event} | ||||
|                                               </div> | ||||
|                                           </dd> | ||||
|                                       </div>`; | ||||
|                                   }) | ||||
|                                 : html`<div class="pf-c-description-list__group"> | ||||
|                                       <dt class="pf-c-description-list__term"> | ||||
|                                           <span class="pf-c-description-list__text" | ||||
|                                               >${t`No log messages.`}</span | ||||
|                                           > | ||||
|                                       </dt> | ||||
|                                   </div>`} | ||||
|                         </dl> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </ak-form-element-horizontal> | ||||
|         `; | ||||
|     } | ||||
|  | ||||
|     renderForm(): TemplateResult { | ||||
|         return html`<form class="pf-c-form pf-m-horizontal"> | ||||
|             <ak-form-element-horizontal label=${t`User`} ?required=${true} name="forUser"> | ||||
|                 <select class="pf-c-form-control"> | ||||
|                     ${until( | ||||
|                         new CoreApi(DEFAULT_CONFIG) | ||||
|                             .coreUsersList({ | ||||
|                                 ordering: "username", | ||||
|                             }) | ||||
|                             .then((users) => { | ||||
|                                 return users.results.map((user) => { | ||||
|                                     return html`<option | ||||
|                                         ?selected=${user.pk.toString() === this.request?.toString()} | ||||
|                                         value=${user.pk} | ||||
|                                     > | ||||
|                                         ${UserOption(user)} | ||||
|                                     </option>`; | ||||
|                                 }); | ||||
|                             }), | ||||
|                         html`<option>${t`Loading...`}</option>`, | ||||
|                     )} | ||||
|                 </select> | ||||
|             </ak-form-element-horizontal> | ||||
|             ${this.result ? this.renderResult() : html``} | ||||
|         </form>`; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										268
									
								
								web/src/admin/applications/ApplicationForm.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										268
									
								
								web/src/admin/applications/ApplicationForm.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,268 @@ | ||||
| import "@goauthentik/admin/providers/ProviderWizard"; | ||||
| import { DEFAULT_CONFIG, config } from "@goauthentik/common/api/config"; | ||||
| import { first } from "@goauthentik/common/utils"; | ||||
| import "@goauthentik/elements/forms/FormGroup"; | ||||
| import "@goauthentik/elements/forms/HorizontalFormElement"; | ||||
| import "@goauthentik/elements/forms/ModalForm"; | ||||
| import { ModelForm } from "@goauthentik/elements/forms/ModelForm"; | ||||
| import "@goauthentik/elements/forms/ProxyForm"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { TemplateResult, html } from "lit"; | ||||
| import { customElement, property } from "lit/decorators.js"; | ||||
| import { ifDefined } from "lit/directives/if-defined.js"; | ||||
| import { until } from "lit/directives/until.js"; | ||||
|  | ||||
| import { | ||||
|     Application, | ||||
|     CapabilitiesEnum, | ||||
|     CoreApi, | ||||
|     PolicyEngineMode, | ||||
|     Provider, | ||||
|     ProvidersApi, | ||||
| } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-application-form") | ||||
| export class ApplicationForm extends ModelForm<Application, string> { | ||||
|     loadInstance(pk: string): Promise<Application> { | ||||
|         return new CoreApi(DEFAULT_CONFIG).coreApplicationsRetrieve({ | ||||
|             slug: pk, | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     @property({ attribute: false }) | ||||
|     provider?: number; | ||||
|  | ||||
|     @property({ type: Boolean }) | ||||
|     clearIcon = false; | ||||
|  | ||||
|     getSuccessMessage(): string { | ||||
|         if (this.instance) { | ||||
|             return t`Successfully updated application.`; | ||||
|         } else { | ||||
|             return t`Successfully created application.`; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     send = async (data: Application): Promise<Application | void> => { | ||||
|         let app: Application; | ||||
|         if (this.instance) { | ||||
|             app = await new CoreApi(DEFAULT_CONFIG).coreApplicationsUpdate({ | ||||
|                 slug: this.instance.slug, | ||||
|                 applicationRequest: data, | ||||
|             }); | ||||
|         } else { | ||||
|             app = await new CoreApi(DEFAULT_CONFIG).coreApplicationsCreate({ | ||||
|                 applicationRequest: data, | ||||
|             }); | ||||
|         } | ||||
|         const c = await config(); | ||||
|         if (c.capabilities.includes(CapabilitiesEnum.SaveMedia)) { | ||||
|             const icon = this.getFormFiles()["metaIcon"]; | ||||
|             if (icon || this.clearIcon) { | ||||
|                 await new CoreApi(DEFAULT_CONFIG).coreApplicationsSetIconCreate({ | ||||
|                     slug: app.slug, | ||||
|                     file: icon, | ||||
|                     clear: this.clearIcon, | ||||
|                 }); | ||||
|             } | ||||
|         } else { | ||||
|             await new CoreApi(DEFAULT_CONFIG).coreApplicationsSetIconUrlCreate({ | ||||
|                 slug: app.slug, | ||||
|                 filePathRequest: { | ||||
|                     url: data.metaIcon || "", | ||||
|                 }, | ||||
|             }); | ||||
|         } | ||||
|         return app; | ||||
|     }; | ||||
|  | ||||
|     groupProviders(providers: Provider[]): TemplateResult { | ||||
|         const m = new Map<string, Provider[]>(); | ||||
|         providers.forEach((p) => { | ||||
|             if (!m.has(p.verboseName || "")) { | ||||
|                 m.set(p.verboseName || "", []); | ||||
|             } | ||||
|             const tProviders = m.get(p.verboseName || "") || []; | ||||
|             tProviders.push(p); | ||||
|         }); | ||||
|         return html` | ||||
|             ${Array.from(m).map(([group, providers]) => { | ||||
|                 return html`<optgroup label=${group}> | ||||
|                     ${providers.map((p) => { | ||||
|                         const selected = this.instance?.provider === p.pk || this.provider === p.pk; | ||||
|                         return html`<option ?selected=${selected} value=${ifDefined(p.pk)}> | ||||
|                             ${p.name} | ||||
|                         </option>`; | ||||
|                     })} | ||||
|                 </optgroup>`; | ||||
|             })} | ||||
|         `; | ||||
|     } | ||||
|  | ||||
|     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="${ifDefined(this.instance?.name)}" | ||||
|                     class="pf-c-form-control" | ||||
|                     required | ||||
|                 /> | ||||
|                 <p class="pf-c-form__helper-text">${t`Application's display Name.`}</p> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-element-horizontal label=${t`Slug`} ?required=${true} name="slug"> | ||||
|                 <input | ||||
|                     type="text" | ||||
|                     value="${ifDefined(this.instance?.slug)}" | ||||
|                     class="pf-c-form-control" | ||||
|                     required | ||||
|                 /> | ||||
|                 <p class="pf-c-form__helper-text">${t`Internal application name, used in URLs.`}</p> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-element-horizontal label=${t`Group`} name="group"> | ||||
|                 <input | ||||
|                     type="text" | ||||
|                     value="${ifDefined(this.instance?.group)}" | ||||
|                     class="pf-c-form-control" | ||||
|                 /> | ||||
|                 <p class="pf-c-form__helper-text"> | ||||
|                     ${t`Optionally enter a group name. Applications with identical groups are shown grouped together.`} | ||||
|                 </p> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-element-horizontal label=${t`Provider`} name="provider"> | ||||
|                 <select class="pf-c-form-control"> | ||||
|                     <option value="" ?selected=${this.instance?.provider === undefined}> | ||||
|                         --------- | ||||
|                     </option> | ||||
|                     ${until( | ||||
|                         new ProvidersApi(DEFAULT_CONFIG).providersAllList({}).then((providers) => { | ||||
|                             return this.groupProviders(providers.results); | ||||
|                         }), | ||||
|                         html`<option>${t`Loading...`}</option>`, | ||||
|                     )} | ||||
|                 </select> | ||||
|                 <p class="pf-c-form__helper-text"> | ||||
|                     ${t`Select a provider that this application should use. Alternatively, create a new provider.`} | ||||
|                 </p> | ||||
|                 <ak-provider-wizard | ||||
|                     .finalHandler=${async () => { | ||||
|                         this.requestUpdate(); | ||||
|                     }} | ||||
|                     createText=${t`Create provider`} | ||||
|                 > | ||||
|                 </ak-provider-wizard> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-element-horizontal | ||||
|                 label=${t`Policy engine mode`} | ||||
|                 ?required=${true} | ||||
|                 name="policyEngineMode" | ||||
|             > | ||||
|                 <select class="pf-c-form-control"> | ||||
|                     <option | ||||
|                         value=${PolicyEngineMode.Any} | ||||
|                         ?selected=${this.instance?.policyEngineMode === PolicyEngineMode.Any} | ||||
|                     > | ||||
|                         ${t`ANY, any policy must match to grant access.`} | ||||
|                     </option> | ||||
|                     <option | ||||
|                         value=${PolicyEngineMode.All} | ||||
|                         ?selected=${this.instance?.policyEngineMode === PolicyEngineMode.All} | ||||
|                     > | ||||
|                         ${t`ALL, all policies must match to grant access.`} | ||||
|                     </option> | ||||
|                 </select> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-group .expanded=${true}> | ||||
|                 <span slot="header"> ${t`UI settings`} </span> | ||||
|                 <div slot="body" class="pf-c-form"> | ||||
|                     <ak-form-element-horizontal label=${t`Launch URL`} name="metaLaunchUrl"> | ||||
|                         <input | ||||
|                             type="text" | ||||
|                             value="${ifDefined(this.instance?.metaLaunchUrl)}" | ||||
|                             class="pf-c-form-control" | ||||
|                         /> | ||||
|                         <p class="pf-c-form__helper-text"> | ||||
|                             ${t`If left empty, authentik will try to extract the launch URL based on the selected provider.`} | ||||
|                         </p> | ||||
|                     </ak-form-element-horizontal> | ||||
|                     <ak-form-element-horizontal label=${t`Open in new tab`} name="openInNewTab"> | ||||
|                         <input type="checkbox" ?checked=${this.instance?.openInNewTab} /> | ||||
|                         <p class="pf-c-form__helper-text"> | ||||
|                             ${t`If checked, the launch URL will open in a new browser tab or window from the user's application library.`} | ||||
|                         </p> | ||||
|                     </ak-form-element-horizontal> | ||||
|                     ${until( | ||||
|                         config().then((c) => { | ||||
|                             if (c.capabilities.includes(CapabilitiesEnum.SaveMedia)) { | ||||
|                                 return html`<ak-form-element-horizontal | ||||
|                                         label=${t`Icon`} | ||||
|                                         name="metaIcon" | ||||
|                                     > | ||||
|                                         <input type="file" value="" class="pf-c-form-control" /> | ||||
|                                         ${this.instance?.metaIcon | ||||
|                                             ? html` | ||||
|                                                   <p class="pf-c-form__helper-text"> | ||||
|                                                       ${t`Currently set to:`} | ||||
|                                                       ${this.instance?.metaIcon} | ||||
|                                                   </p> | ||||
|                                               ` | ||||
|                                             : html``} | ||||
|                                     </ak-form-element-horizontal> | ||||
|                                     ${this.instance?.metaIcon | ||||
|                                         ? html` | ||||
|                                               <ak-form-element-horizontal> | ||||
|                                                   <div class="pf-c-check"> | ||||
|                                                       <input | ||||
|                                                           type="checkbox" | ||||
|                                                           class="pf-c-check__input" | ||||
|                                                           @change=${(ev: Event) => { | ||||
|                                                               const target = | ||||
|                                                                   ev.target as HTMLInputElement; | ||||
|                                                               this.clearIcon = target.checked; | ||||
|                                                           }} | ||||
|                                                       /> | ||||
|                                                       <label class="pf-c-check__label"> | ||||
|                                                           ${t`Clear icon`} | ||||
|                                                       </label> | ||||
|                                                   </div> | ||||
|                                                   <p class="pf-c-form__helper-text"> | ||||
|                                                       ${t`Delete currently set icon.`} | ||||
|                                                   </p> | ||||
|                                               </ak-form-element-horizontal> | ||||
|                                           ` | ||||
|                                         : html``}`; | ||||
|                             } | ||||
|                             return html`<ak-form-element-horizontal | ||||
|                                 label=${t`Icon`} | ||||
|                                 name="metaIcon" | ||||
|                             > | ||||
|                                 <input | ||||
|                                     type="text" | ||||
|                                     value="${first(this.instance?.metaIcon, "")}" | ||||
|                                     class="pf-c-form-control" | ||||
|                                 /> | ||||
|                                 <p class="pf-c-form__helper-text"> | ||||
|                                     ${t`Either input a full URL, a relative path, or use 'fa://fa-test' to use the Font Awesome icon "fa-test".`} | ||||
|                                 </p> | ||||
|                             </ak-form-element-horizontal>`; | ||||
|                         }), | ||||
|                     )} | ||||
|                     <ak-form-element-horizontal label=${t`Publisher`} name="metaPublisher"> | ||||
|                         <input | ||||
|                             type="text" | ||||
|                             value="${ifDefined(this.instance?.metaPublisher)}" | ||||
|                             class="pf-c-form-control" | ||||
|                         /> | ||||
|                     </ak-form-element-horizontal> | ||||
|                     <ak-form-element-horizontal label=${t`Description`} name="metaDescription"> | ||||
|                         <textarea class="pf-c-form-control"> | ||||
| ${ifDefined(this.instance?.metaDescription)}</textarea | ||||
|                         > | ||||
|                     </ak-form-element-horizontal> | ||||
|                 </div> | ||||
|             </ak-form-group> | ||||
|         </form>`; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										177
									
								
								web/src/admin/applications/ApplicationListPage.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										177
									
								
								web/src/admin/applications/ApplicationListPage.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,177 @@ | ||||
| import "@goauthentik/admin/applications/ApplicationForm"; | ||||
| import "@goauthentik/admin/applications/wizard/ApplicationWizard"; | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { uiConfig } from "@goauthentik/common/ui/config"; | ||||
| import MDApplication from "@goauthentik/docs/core/applications.md"; | ||||
| import "@goauthentik/elements/Markdown"; | ||||
| import "@goauthentik/elements/buttons/SpinnerButton"; | ||||
| import "@goauthentik/elements/forms/DeleteBulkForm"; | ||||
| import "@goauthentik/elements/forms/ModalForm"; | ||||
| import { getURLParam } from "@goauthentik/elements/router/RouteMatch"; | ||||
| import { PaginatedResponse } from "@goauthentik/elements/table/Table"; | ||||
| import { TableColumn } from "@goauthentik/elements/table/Table"; | ||||
| import { TablePage } from "@goauthentik/elements/table/TablePage"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { CSSResult, TemplateResult, css, html } from "lit"; | ||||
| import { customElement, property } from "lit/decorators.js"; | ||||
| import { ifDefined } from "lit/directives/if-defined.js"; | ||||
|  | ||||
| import AKGlobal from "@goauthentik/common/styles/authentik.css"; | ||||
| import PFAvatar from "@patternfly/patternfly/components/Avatar/avatar.css"; | ||||
| import PFCard from "@patternfly/patternfly/components/Card/card.css"; | ||||
|  | ||||
| import { Application, CoreApi } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-application-list") | ||||
| export class ApplicationListPage extends TablePage<Application> { | ||||
|     searchEnabled(): boolean { | ||||
|         return true; | ||||
|     } | ||||
|     pageTitle(): string { | ||||
|         return t`Applications`; | ||||
|     } | ||||
|     pageDescription(): string { | ||||
|         return t`External Applications which use authentik as Identity-Provider, utilizing protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.`; | ||||
|     } | ||||
|     pageIcon(): string { | ||||
|         return "pf-icon pf-icon-applications"; | ||||
|     } | ||||
|  | ||||
|     checkbox = true; | ||||
|  | ||||
|     @property() | ||||
|     order = "name"; | ||||
|  | ||||
|     async apiEndpoint(page: number): Promise<PaginatedResponse<Application>> { | ||||
|         return new CoreApi(DEFAULT_CONFIG).coreApplicationsList({ | ||||
|             ordering: this.order, | ||||
|             page: page, | ||||
|             pageSize: (await uiConfig()).pagination.perPage, | ||||
|             search: this.search || "", | ||||
|             superuserFullList: true, | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     static get styles(): CSSResult[] { | ||||
|         return super.styles.concat( | ||||
|             PFAvatar, | ||||
|             PFCard, | ||||
|             AKGlobal, | ||||
|             css` | ||||
|                 tr td:first-child { | ||||
|                     width: auto; | ||||
|                     min-width: 0px; | ||||
|                     text-align: center; | ||||
|                     vertical-align: middle; | ||||
|                 } | ||||
|             `, | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     columns(): TableColumn[] { | ||||
|         return [ | ||||
|             new TableColumn(""), | ||||
|             new TableColumn(t`Name`, "name"), | ||||
|             new TableColumn(t`Group`, "group"), | ||||
|             new TableColumn(t`Provider`), | ||||
|             new TableColumn(t`Provider Type`), | ||||
|             new TableColumn(t`Actions`), | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     renderSidebarAfter(): TemplateResult { | ||||
|         // Rendering the wizard with .open here, as if we set the attribute in | ||||
|         // renderObjectCreate() it'll open two wizards, since that function gets called twice | ||||
|         return html`<ak-application-wizard | ||||
|                 .open=${getURLParam("createWizard", false)} | ||||
|                 .showButton=${false} | ||||
|             ></ak-application-wizard> | ||||
|             <div class="pf-c-sidebar__panel pf-m-width-25"> | ||||
|                 <div class="pf-c-card"> | ||||
|                     <div class="pf-c-card__title">${t`About applications`}</div> | ||||
|                     <div class="pf-c-card__body"> | ||||
|                         <ak-markdown .md=${MDApplication}></ak-markdown> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </div>`; | ||||
|     } | ||||
|  | ||||
|     renderToolbarSelected(): TemplateResult { | ||||
|         const disabled = this.selectedElements.length < 1; | ||||
|         return html`<ak-forms-delete-bulk | ||||
|             objectLabel=${t`Application(s)`} | ||||
|             .objects=${this.selectedElements} | ||||
|             .usedBy=${(item: Application) => { | ||||
|                 return new CoreApi(DEFAULT_CONFIG).coreApplicationsUsedByList({ | ||||
|                     slug: item.slug, | ||||
|                 }); | ||||
|             }} | ||||
|             .delete=${(item: Application) => { | ||||
|                 return new CoreApi(DEFAULT_CONFIG).coreApplicationsDestroy({ | ||||
|                     slug: item.slug, | ||||
|                 }); | ||||
|             }} | ||||
|         > | ||||
|             <button ?disabled=${disabled} slot="trigger" class="pf-c-button pf-m-danger"> | ||||
|                 ${t`Delete`} | ||||
|             </button> | ||||
|         </ak-forms-delete-bulk>`; | ||||
|     } | ||||
|  | ||||
|     renderIcon(item: Application): TemplateResult { | ||||
|         if (item?.metaIcon) { | ||||
|             if (item.metaIcon.startsWith("fa://")) { | ||||
|                 const icon = item.metaIcon.replaceAll("fa://", ""); | ||||
|                 return html`<i class="fas ${icon}"></i>`; | ||||
|             } | ||||
|             return html`<img | ||||
|                 class="app-icon pf-c-avatar" | ||||
|                 src="${ifDefined(item.metaIcon)}" | ||||
|                 alt="${t`Application Icon`}" | ||||
|             />`; | ||||
|         } | ||||
|         return html`<i class="fas fa-share-square"></i>`; | ||||
|     } | ||||
|  | ||||
|     row(item: Application): TemplateResult[] { | ||||
|         return [ | ||||
|             this.renderIcon(item), | ||||
|             html`<a href="#/core/applications/${item.slug}"> | ||||
|                 <div>${item.name}</div> | ||||
|                 ${item.metaPublisher ? html`<small>${item.metaPublisher}</small>` : html``} | ||||
|             </a>`, | ||||
|             html`${item.group || t`-`}`, | ||||
|             item.provider | ||||
|                 ? html`<a href="#/core/providers/${item.providerObj?.pk}"> | ||||
|                       ${item.providerObj?.name} | ||||
|                   </a>` | ||||
|                 : html`-`, | ||||
|             html`${item.providerObj?.verboseName || t`-`}`, | ||||
|             html`<ak-forms-modal> | ||||
|                     <span slot="submit"> ${t`Update`} </span> | ||||
|                     <span slot="header"> ${t`Update Application`} </span> | ||||
|                     <ak-application-form slot="form" .instancePk=${item.slug}> | ||||
|                     </ak-application-form> | ||||
|                     <button slot="trigger" class="pf-c-button pf-m-plain"> | ||||
|                         <i class="fas fa-edit"></i> | ||||
|                     </button> | ||||
|                 </ak-forms-modal> | ||||
|                 ${item.launchUrl | ||||
|                     ? html`<a href=${item.launchUrl} target="_blank" class="pf-c-button pf-m-plain"> | ||||
|                           <i class="fas fa-share-square"></i> | ||||
|                       </a>` | ||||
|                     : html``}`, | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     renderObjectCreate(): TemplateResult { | ||||
|         return html`<ak-forms-modal .open=${getURLParam("createForm", false)}> | ||||
|             <span slot="submit"> ${t`Create`} </span> | ||||
|             <span slot="header"> ${t`Create Application`} </span> | ||||
|             <ak-application-form slot="form"> </ak-application-form> | ||||
|             <button slot="trigger" class="pf-c-button pf-m-primary">${t`Create`}</button> | ||||
|         </ak-forms-modal>`; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										265
									
								
								web/src/admin/applications/ApplicationViewPage.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										265
									
								
								web/src/admin/applications/ApplicationViewPage.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,265 @@ | ||||
| import "@goauthentik/admin/applications/ApplicationCheckAccessForm"; | ||||
| import "@goauthentik/admin/applications/ApplicationForm"; | ||||
| import "@goauthentik/admin/policies/BoundPoliciesList"; | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { AKElement } from "@goauthentik/elements/Base"; | ||||
| import "@goauthentik/elements/EmptyState"; | ||||
| import "@goauthentik/elements/PageHeader"; | ||||
| import "@goauthentik/elements/Tabs"; | ||||
| import "@goauthentik/elements/buttons/SpinnerButton"; | ||||
| import "@goauthentik/elements/charts/ApplicationAuthorizeChart"; | ||||
| import "@goauthentik/elements/events/ObjectChangelog"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { CSSResult, TemplateResult, html } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators.js"; | ||||
| import { ifDefined } from "lit/directives/if-defined.js"; | ||||
|  | ||||
| import AKGlobal from "@goauthentik/common/styles/authentik.css"; | ||||
| import PFBanner from "@patternfly/patternfly/components/Banner/banner.css"; | ||||
| import PFButton from "@patternfly/patternfly/components/Button/button.css"; | ||||
| import PFCard from "@patternfly/patternfly/components/Card/card.css"; | ||||
| import PFContent from "@patternfly/patternfly/components/Content/content.css"; | ||||
| import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css"; | ||||
| import PFPage from "@patternfly/patternfly/components/Page/page.css"; | ||||
| import PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css"; | ||||
| import PFBase from "@patternfly/patternfly/patternfly-base.css"; | ||||
|  | ||||
| import { Application, CoreApi, OutpostsApi } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-application-view") | ||||
| export class ApplicationViewPage extends AKElement { | ||||
|     @property() | ||||
|     set applicationSlug(value: string) { | ||||
|         new CoreApi(DEFAULT_CONFIG) | ||||
|             .coreApplicationsRetrieve({ | ||||
|                 slug: value, | ||||
|             }) | ||||
|             .then((app) => { | ||||
|                 this.application = app; | ||||
|                 if ( | ||||
|                     app.providerObj && | ||||
|                     [ | ||||
|                         "authentik_providers_proxy.proxyprovider", | ||||
|                         "authentik_providers_ldap.ldapprovider", | ||||
|                     ].includes(app.providerObj.metaModelName) | ||||
|                 ) { | ||||
|                     new OutpostsApi(DEFAULT_CONFIG) | ||||
|                         .outpostsInstancesList({ | ||||
|                             providersByPk: [app.provider || 0], | ||||
|                             pageSize: 1, | ||||
|                         }) | ||||
|                         .then((outposts) => { | ||||
|                             if (outposts.pagination.count < 1) { | ||||
|                                 this.missingOutpost = true; | ||||
|                             } | ||||
|                         }); | ||||
|                 } | ||||
|             }); | ||||
|     } | ||||
|  | ||||
|     @property({ attribute: false }) | ||||
|     application!: Application; | ||||
|  | ||||
|     @state() | ||||
|     missingOutpost = false; | ||||
|  | ||||
|     static get styles(): CSSResult[] { | ||||
|         return [ | ||||
|             PFBase, | ||||
|             PFBanner, | ||||
|             PFPage, | ||||
|             PFContent, | ||||
|             PFButton, | ||||
|             PFDescriptionList, | ||||
|             PFGrid, | ||||
|             PFCard, | ||||
|             AKGlobal, | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     render(): TemplateResult { | ||||
|         return html`<ak-page-header | ||||
|                 icon=${this.application?.metaIcon || ""} | ||||
|                 header=${this.application?.name || t`Loading`} | ||||
|                 description=${ifDefined(this.application?.metaPublisher)} | ||||
|                 .iconImage=${true} | ||||
|             > | ||||
|             </ak-page-header> | ||||
|             ${this.renderApp()}`; | ||||
|     } | ||||
|  | ||||
|     renderApp(): TemplateResult { | ||||
|         if (!this.application) { | ||||
|             return html`<ak-empty-state ?loading="${true}" header=${t`Loading`}> </ak-empty-state>`; | ||||
|         } | ||||
|         return html`<ak-tabs> | ||||
|             ${this.missingOutpost | ||||
|                 ? html`<div slot="header" class="pf-c-banner pf-m-warning"> | ||||
|                       ${t`Warning: Application is not used by any Outpost.`} | ||||
|                   </div>` | ||||
|                 : html``} | ||||
|             <section | ||||
|                 slot="page-overview" | ||||
|                 data-tab-title="${t`Overview`}" | ||||
|                 class="pf-c-page__main-section pf-m-no-padding-mobile" | ||||
|             > | ||||
|                 <div class="pf-l-grid pf-m-gutter"> | ||||
|                     <div | ||||
|                         class="pf-c-card pf-l-grid__item pf-m-12-col pf-m-2-col-on-xl pf-m-2-col-on-2xl" | ||||
|                     > | ||||
|                         <div class="pf-c-card__title">${t`Related`}</div> | ||||
|                         <div class="pf-c-card__body"> | ||||
|                             <dl class="pf-c-description-list"> | ||||
|                                 ${this.application.providerObj | ||||
|                                     ? html`<div class="pf-c-description-list__group"> | ||||
|                                           <dt class="pf-c-description-list__term"> | ||||
|                                               <span class="pf-c-description-list__text" | ||||
|                                                   >${t`Provider`}</span | ||||
|                                               > | ||||
|                                           </dt> | ||||
|                                           <dd class="pf-c-description-list__description"> | ||||
|                                               <div class="pf-c-description-list__text"> | ||||
|                                                   <a | ||||
|                                                       href="#/core/providers/${this.application | ||||
|                                                           .providerObj?.pk}" | ||||
|                                                   > | ||||
|                                                       ${this.application.providerObj?.name} | ||||
|                                                       (${this.application.providerObj?.verboseName}) | ||||
|                                                   </a> | ||||
|                                               </div> | ||||
|                                           </dd> | ||||
|                                       </div>` | ||||
|                                     : html``} | ||||
|                                 <div class="pf-c-description-list__group"> | ||||
|                                     <dt class="pf-c-description-list__term"> | ||||
|                                         <span class="pf-c-description-list__text" | ||||
|                                             >${t`Policy engine mode`}</span | ||||
|                                         > | ||||
|                                     </dt> | ||||
|                                     <dd class="pf-c-description-list__description"> | ||||
|                                         <div class="pf-c-description-list__text"> | ||||
|                                             ${this.application.policyEngineMode?.toUpperCase()} | ||||
|                                         </div> | ||||
|                                     </dd> | ||||
|                                 </div> | ||||
|                                 <div class="pf-c-description-list__group"> | ||||
|                                     <dt class="pf-c-description-list__term"> | ||||
|                                         <span class="pf-c-description-list__text">${t`Edit`}</span> | ||||
|                                     </dt> | ||||
|                                     <dd class="pf-c-description-list__description"> | ||||
|                                         <div class="pf-c-description-list__text"> | ||||
|                                             <ak-forms-modal> | ||||
|                                                 <span slot="submit"> ${t`Update`} </span> | ||||
|                                                 <span slot="header"> | ||||
|                                                     ${t`Update Application`} | ||||
|                                                 </span> | ||||
|                                                 <ak-application-form | ||||
|                                                     slot="form" | ||||
|                                                     .instancePk=${this.application.slug} | ||||
|                                                 > | ||||
|                                                 </ak-application-form> | ||||
|                                                 <button | ||||
|                                                     slot="trigger" | ||||
|                                                     class="pf-c-button pf-m-secondary" | ||||
|                                                 > | ||||
|                                                     ${t`Edit`} | ||||
|                                                 </button> | ||||
|                                             </ak-forms-modal> | ||||
|                                         </div> | ||||
|                                     </dd> | ||||
|                                 </div> | ||||
|                                 <div class="pf-c-description-list__group"> | ||||
|                                     <dt class="pf-c-description-list__term"> | ||||
|                                         <span class="pf-c-description-list__text" | ||||
|                                             >${t`Check access`}</span | ||||
|                                         > | ||||
|                                     </dt> | ||||
|                                     <dd class="pf-c-description-list__description"> | ||||
|                                         <div class="pf-c-description-list__text"> | ||||
|                                             <ak-forms-modal .closeAfterSuccessfulSubmit=${false}> | ||||
|                                                 <span slot="submit"> ${t`Check`} </span> | ||||
|                                                 <span slot="header"> | ||||
|                                                     ${t`Check Application access`} | ||||
|                                                 </span> | ||||
|                                                 <ak-application-check-access-form | ||||
|                                                     slot="form" | ||||
|                                                     .application=${this.application} | ||||
|                                                 > | ||||
|                                                 </ak-application-check-access-form> | ||||
|                                                 <button | ||||
|                                                     slot="trigger" | ||||
|                                                     class="pf-c-button pf-m-secondary" | ||||
|                                                 > | ||||
|                                                     ${t`Test`} | ||||
|                                                 </button> | ||||
|                                             </ak-forms-modal> | ||||
|                                         </div> | ||||
|                                     </dd> | ||||
|                                 </div> | ||||
|                                 ${this.application.launchUrl | ||||
|                                     ? html`<div class="pf-c-description-list__group"> | ||||
|                                           <dt class="pf-c-description-list__term"> | ||||
|                                               <span class="pf-c-description-list__text" | ||||
|                                                   >${t`Launch`}</span | ||||
|                                               > | ||||
|                                           </dt> | ||||
|                                           <dd class="pf-c-description-list__description"> | ||||
|                                               <div class="pf-c-description-list__text"> | ||||
|                                                   <a | ||||
|                                                       target="_blank" | ||||
|                                                       href=${this.application.launchUrl} | ||||
|                                                       slot="trigger" | ||||
|                                                       class="pf-c-button pf-m-secondary" | ||||
|                                                   > | ||||
|                                                       ${t`Launch`} | ||||
|                                                   </a> | ||||
|                                               </div> | ||||
|                                           </dd> | ||||
|                                       </div>` | ||||
|                                     : html``} | ||||
|                             </dl> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                     <div | ||||
|                         class="pf-c-card pf-l-grid__item pf-m-12-col pf-m-10-col-on-xl pf-m-10-col-on-2xl" | ||||
|                     > | ||||
|                         <div class="pf-c-card__title">${t`Logins over the last 24 hours`}</div> | ||||
|                         <div class="pf-c-card__body"> | ||||
|                             ${this.application && | ||||
|                             html` <ak-charts-application-authorize | ||||
|                                 applicationSlug=${this.application.slug} | ||||
|                             > | ||||
|                             </ak-charts-application-authorize>`} | ||||
|                         </div> | ||||
|                     </div> | ||||
|                     <div class="pf-c-card pf-l-grid__item pf-m-12-col"> | ||||
|                         <div class="pf-c-card__title">${t`Changelog`}</div> | ||||
|                         <div class="pf-c-card__body"> | ||||
|                             <ak-object-changelog | ||||
|                                 targetModelPk=${this.application.pk || ""} | ||||
|                                 targetModelApp="authentik_core" | ||||
|                                 targetModelName="application" | ||||
|                             > | ||||
|                             </ak-object-changelog> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </section> | ||||
|             <section | ||||
|                 slot="page-policy-bindings" | ||||
|                 data-tab-title="${t`Policy / Group / User Bindings`}" | ||||
|                 class="pf-c-page__main-section pf-m-no-padding-mobile" | ||||
|             > | ||||
|                 <div class="pf-c-card"> | ||||
|                     <div class="pf-c-card__title"> | ||||
|                         ${t`These policies control which users can access this application.`} | ||||
|                     </div> | ||||
|                     <ak-bound-policies-list .target=${this.application.pk}> | ||||
|                     </ak-bound-policies-list> | ||||
|                 </div> | ||||
|             </section> | ||||
|         </ak-tabs>`; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										66
									
								
								web/src/admin/applications/wizard/ApplicationWizard.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								web/src/admin/applications/wizard/ApplicationWizard.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,66 @@ | ||||
| import "@goauthentik/admin/applications/wizard/InitialApplicationWizardPage"; | ||||
| import "@goauthentik/admin/applications/wizard/TypeApplicationWizardPage"; | ||||
| import "@goauthentik/admin/applications/wizard/ldap/TypeLDAPApplicationWizardPage"; | ||||
| import "@goauthentik/admin/applications/wizard/link/TypeLinkApplicationWizardPage"; | ||||
| import "@goauthentik/admin/applications/wizard/oauth/TypeOAuthAPIApplicationWizardPage"; | ||||
| import "@goauthentik/admin/applications/wizard/oauth/TypeOAuthApplicationWizardPage"; | ||||
| import "@goauthentik/admin/applications/wizard/oauth/TypeOAuthCodeApplicationWizardPage"; | ||||
| import "@goauthentik/admin/applications/wizard/oauth/TypeOAuthImplicitApplicationWizardPage"; | ||||
| import "@goauthentik/admin/applications/wizard/proxy/TypeProxyApplicationWizardPage"; | ||||
| import "@goauthentik/admin/applications/wizard/saml/TypeSAMLApplicationWizardPage"; | ||||
| import "@goauthentik/admin/applications/wizard/saml/TypeSAMLConfigApplicationWizardPage"; | ||||
| import "@goauthentik/admin/applications/wizard/saml/TypeSAMLImportApplicationWizardPage"; | ||||
| import { AKElement } from "@goauthentik/elements/Base"; | ||||
| import "@goauthentik/elements/wizard/Wizard"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { customElement } from "@lit/reactive-element/decorators/custom-element.js"; | ||||
| import { CSSResult, TemplateResult, html } from "lit"; | ||||
| import { property } from "lit/decorators.js"; | ||||
|  | ||||
| import AKGlobal from "@goauthentik/common/styles/authentik.css"; | ||||
| import PFButton from "@patternfly/patternfly/components/Button/button.css"; | ||||
| import PFRadio from "@patternfly/patternfly/components/Radio/radio.css"; | ||||
| import PFBase from "@patternfly/patternfly/patternfly-base.css"; | ||||
|  | ||||
| @customElement("ak-application-wizard") | ||||
| export class ApplicationWizard extends AKElement { | ||||
|     static get styles(): CSSResult[] { | ||||
|         return [PFBase, PFButton, AKGlobal, PFRadio]; | ||||
|     } | ||||
|  | ||||
|     @property({ type: Boolean }) | ||||
|     open = false; | ||||
|  | ||||
|     @property() | ||||
|     createText = t`Create`; | ||||
|  | ||||
|     @property({ type: Boolean }) | ||||
|     showButton = true; | ||||
|  | ||||
|     @property({ attribute: false }) | ||||
|     finalHandler: () => Promise<void> = () => { | ||||
|         return Promise.resolve(); | ||||
|     }; | ||||
|  | ||||
|     render(): TemplateResult { | ||||
|         return html` | ||||
|             <ak-wizard | ||||
|                 .open=${this.open} | ||||
|                 .steps=${["ak-application-wizard-initial", "ak-application-wizard-type"]} | ||||
|                 header=${t`New application`} | ||||
|                 description=${t`Create a new application.`} | ||||
|                 .finalHandler=${() => { | ||||
|                     return this.finalHandler(); | ||||
|                 }} | ||||
|             > | ||||
|                 ${this.showButton | ||||
|                     ? html`<button slot="trigger" class="pf-c-button pf-m-primary"> | ||||
|                           ${this.createText} | ||||
|                       </button>` | ||||
|                     : html``} | ||||
|             </ak-wizard> | ||||
|         `; | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,73 @@ | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { convertToSlug } from "@goauthentik/common/utils"; | ||||
| import { KeyUnknown } from "@goauthentik/elements/forms/Form"; | ||||
| import "@goauthentik/elements/forms/FormGroup"; | ||||
| import "@goauthentik/elements/forms/HorizontalFormElement"; | ||||
| import { WizardFormPage } from "@goauthentik/elements/wizard/WizardFormPage"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { customElement } from "@lit/reactive-element/decorators/custom-element.js"; | ||||
| import { TemplateResult, html } from "lit"; | ||||
|  | ||||
| import { ApplicationRequest, CoreApi, Provider } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-application-wizard-initial") | ||||
| export class InitialApplicationWizardPage extends WizardFormPage { | ||||
|     sidebarLabel = () => t`Application details`; | ||||
|  | ||||
|     nextDataCallback = async (data: KeyUnknown): Promise<boolean> => { | ||||
|         const name = data.name as string; | ||||
|         let slug = convertToSlug(name || ""); | ||||
|         // Check if an application with the generated slug already exists | ||||
|         const apps = await new CoreApi(DEFAULT_CONFIG).coreApplicationsList({ | ||||
|             search: slug, | ||||
|         }); | ||||
|         if (apps.results.filter((app) => app.slug == slug)) { | ||||
|             slug += "-1"; | ||||
|         } | ||||
|         this.host.state["slug"] = slug; | ||||
|         this.host.state["name"] = name; | ||||
|         this.host.addActionBefore(t`Create application`, async (): Promise<boolean> => { | ||||
|             const req: ApplicationRequest = { | ||||
|                 name: name || "", | ||||
|                 slug: slug, | ||||
|                 metaPublisher: data.metaPublisher as string, | ||||
|                 metaDescription: data.metaDescription as string, | ||||
|             }; | ||||
|             if ("provider" in this.host.state) { | ||||
|                 req.provider = (this.host.state["provider"] as Provider).pk; | ||||
|             } | ||||
|             if ("link" in this.host.state) { | ||||
|                 req.metaLaunchUrl = this.host.state["link"] as string; | ||||
|             } | ||||
|             this.host.state["app"] = await new CoreApi(DEFAULT_CONFIG).coreApplicationsCreate({ | ||||
|                 applicationRequest: req, | ||||
|             }); | ||||
|             return true; | ||||
|         }); | ||||
|         return true; | ||||
|     }; | ||||
|  | ||||
|     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="" class="pf-c-form-control" required /> | ||||
|                     <p class="pf-c-form__helper-text">${t`Application's display Name.`}</p> | ||||
|                 </ak-form-element-horizontal> | ||||
|                 <ak-form-group ?expanded=${true}> | ||||
|                     <span slot="header"> ${t`Additional UI settings`} </span> | ||||
|                     <div slot="body" class="pf-c-form"> | ||||
|                         <ak-form-element-horizontal label=${t`Description`} name="metaDescription"> | ||||
|                             <textarea class="pf-c-form-control"></textarea> | ||||
|                         </ak-form-element-horizontal> | ||||
|                         <ak-form-element-horizontal label=${t`Publisher`} name="metaPublisher"> | ||||
|                             <input type="text" value="" class="pf-c-form-control" /> | ||||
|                         </ak-form-element-horizontal> | ||||
|                     </div> | ||||
|                 </ak-form-group> | ||||
|             </form> | ||||
|         `; | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,81 @@ | ||||
| import { WizardPage } from "@goauthentik/elements/wizard/WizardPage"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { customElement } from "@lit/reactive-element/decorators/custom-element.js"; | ||||
| import { CSSResult, TemplateResult, html } from "lit"; | ||||
|  | ||||
| import AKGlobal from "@goauthentik/common/styles/authentik.css"; | ||||
| import PFButton from "@patternfly/patternfly/components/Button/button.css"; | ||||
| import PFForm from "@patternfly/patternfly/components/Form/form.css"; | ||||
| import PFRadio from "@patternfly/patternfly/components/Radio/radio.css"; | ||||
| import PFBase from "@patternfly/patternfly/patternfly-base.css"; | ||||
|  | ||||
| import { TypeCreate } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-application-wizard-type") | ||||
| export class TypeApplicationWizardPage extends WizardPage { | ||||
|     applicationTypes: TypeCreate[] = [ | ||||
|         { | ||||
|             component: "ak-application-wizard-type-oauth", | ||||
|             name: t`OAuth2/OIDC`, | ||||
|             description: t`Modern applications, APIs and Single-page applications.`, | ||||
|             modelName: "", | ||||
|         }, | ||||
|         { | ||||
|             component: "ak-application-wizard-type-saml", | ||||
|             name: t`SAML`, | ||||
|             description: t`XML-based SSO standard. Use this if your application only supports SAML.`, | ||||
|             modelName: "", | ||||
|         }, | ||||
|         { | ||||
|             component: "ak-application-wizard-type-proxy", | ||||
|             name: t`Proxy`, | ||||
|             description: t`Legacy applications which don't natively support SSO.`, | ||||
|             modelName: "", | ||||
|         }, | ||||
|         { | ||||
|             component: "ak-application-wizard-type-ldap", | ||||
|             name: t`LDAP`, | ||||
|             description: t`Provide an LDAP interface for applications and users to authenticate against.`, | ||||
|             modelName: "", | ||||
|         }, | ||||
|         { | ||||
|             component: "ak-application-wizard-type-link", | ||||
|             name: t`Link`, | ||||
|             description: t`Provide an LDAP interface for applications and users to authenticate against.`, | ||||
|             modelName: "", | ||||
|         }, | ||||
|     ]; | ||||
|  | ||||
|     sidebarLabel = () => t`Authentication method`; | ||||
|  | ||||
|     static get styles(): CSSResult[] { | ||||
|         return [PFBase, PFButton, PFForm, PFRadio, AKGlobal]; | ||||
|     } | ||||
|  | ||||
|     render(): TemplateResult { | ||||
|         return html`<form class="pf-c-form pf-m-horizontal"> | ||||
|             ${this.applicationTypes.map((type) => { | ||||
|                 return html`<div class="pf-c-radio"> | ||||
|                     <input | ||||
|                         class="pf-c-radio__input" | ||||
|                         type="radio" | ||||
|                         name="type" | ||||
|                         id=${type.component} | ||||
|                         @change=${() => { | ||||
|                             this.host.steps = [ | ||||
|                                 "ak-application-wizard-initial", | ||||
|                                 "ak-application-wizard-type", | ||||
|                                 type.component, | ||||
|                             ]; | ||||
|                             this.host.isValid = true; | ||||
|                         }} | ||||
|                     /> | ||||
|                     <label class="pf-c-radio__label" for=${type.component}>${type.name}</label> | ||||
|                     <span class="pf-c-radio__description">${type.description}</span> | ||||
|                 </div>`; | ||||
|             })} | ||||
|         </form>`; | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,74 @@ | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { KeyUnknown } from "@goauthentik/elements/forms/Form"; | ||||
| import "@goauthentik/elements/forms/HorizontalFormElement"; | ||||
| import { WizardFormPage } from "@goauthentik/elements/wizard/WizardFormPage"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { customElement } from "@lit/reactive-element/decorators/custom-element.js"; | ||||
| import { TemplateResult, html } from "lit"; | ||||
|  | ||||
| import { | ||||
|     CoreApi, | ||||
|     FlowDesignationEnum, | ||||
|     FlowsApi, | ||||
|     LDAPProviderRequest, | ||||
|     ProvidersApi, | ||||
|     UserServiceAccountResponse, | ||||
| } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-application-wizard-type-ldap") | ||||
| export class TypeLDAPApplicationWizardPage extends WizardFormPage { | ||||
|     sidebarLabel = () => t`LDAP details`; | ||||
|  | ||||
|     nextDataCallback = async (data: KeyUnknown): Promise<boolean> => { | ||||
|         let name = this.host.state["name"] as string; | ||||
|         // Check if a provider with the name already exists | ||||
|         const providers = await new ProvidersApi(DEFAULT_CONFIG).providersAllList({ | ||||
|             search: name, | ||||
|         }); | ||||
|         if (providers.results.filter((provider) => provider.name == name)) { | ||||
|             name += "-1"; | ||||
|         } | ||||
|         this.host.addActionBefore(t`Create service account`, async (): Promise<boolean> => { | ||||
|             const serviceAccount = await new CoreApi(DEFAULT_CONFIG).coreUsersServiceAccountCreate({ | ||||
|                 userServiceAccountRequest: { | ||||
|                     name: name, | ||||
|                     createGroup: true, | ||||
|                 }, | ||||
|             }); | ||||
|             this.host.state["serviceAccount"] = serviceAccount; | ||||
|             return true; | ||||
|         }); | ||||
|         this.host.addActionBefore(t`Create provider`, async (): Promise<boolean> => { | ||||
|             // Get all flows and default to the implicit authorization | ||||
|             const flows = await new FlowsApi(DEFAULT_CONFIG).flowsInstancesList({ | ||||
|                 designation: FlowDesignationEnum.Authorization, | ||||
|                 ordering: "slug", | ||||
|             }); | ||||
|             const serviceAccount = this.host.state["serviceAccount"] as UserServiceAccountResponse; | ||||
|             const req: LDAPProviderRequest = { | ||||
|                 name: name, | ||||
|                 authorizationFlow: flows.results[0].pk, | ||||
|                 baseDn: data.baseDN as string, | ||||
|                 searchGroup: serviceAccount.groupPk, | ||||
|             }; | ||||
|             const provider = await new ProvidersApi(DEFAULT_CONFIG).providersLdapCreate({ | ||||
|                 lDAPProviderRequest: req, | ||||
|             }); | ||||
|             this.host.state["provider"] = provider; | ||||
|             return true; | ||||
|         }); | ||||
|         return true; | ||||
|     }; | ||||
|  | ||||
|     renderForm(): TemplateResult { | ||||
|         const domainParts = window.location.hostname.split("."); | ||||
|         const defaultBaseDN = domainParts.map((part) => `dc=${part}`).join(","); | ||||
|         return html`<form class="pf-c-form pf-m-horizontal"> | ||||
|             <ak-form-element-horizontal label=${t`Base DN`} name="baseDN" ?required=${true}> | ||||
|                 <input type="text" value="${defaultBaseDN}" class="pf-c-form-control" required /> | ||||
|             </ak-form-element-horizontal> | ||||
|         </form> `; | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,31 @@ | ||||
| import { KeyUnknown } from "@goauthentik/elements/forms/Form"; | ||||
| import "@goauthentik/elements/forms/HorizontalFormElement"; | ||||
| import { WizardFormPage } from "@goauthentik/elements/wizard/WizardFormPage"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { customElement } from "@lit/reactive-element/decorators/custom-element.js"; | ||||
| import { TemplateResult, html } from "lit"; | ||||
|  | ||||
| @customElement("ak-application-wizard-type-link") | ||||
| export class TypeLinkApplicationWizardPage extends WizardFormPage { | ||||
|     sidebarLabel = () => t`Application Link`; | ||||
|  | ||||
|     nextDataCallback = async (data: KeyUnknown): Promise<boolean> => { | ||||
|         this.host.state["link"] = data.link; | ||||
|         return true; | ||||
|     }; | ||||
|  | ||||
|     renderForm(): TemplateResult { | ||||
|         return html` | ||||
|             <form class="pf-c-form pf-m-horizontal"> | ||||
|                 <ak-form-element-horizontal label=${t`Link`} ?required=${true} name="link"> | ||||
|                     <input type="text" value="" class="pf-c-form-control" required /> | ||||
|                     <p class="pf-c-form__helper-text"> | ||||
|                         ${t`URL which will be opened when a user clicks on the application.`} | ||||
|                     </p> | ||||
|                 </ak-form-element-horizontal> | ||||
|             </form> | ||||
|         `; | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,33 @@ | ||||
| import "@goauthentik/elements/forms/HorizontalFormElement"; | ||||
| import { WizardPage } from "@goauthentik/elements/wizard/WizardPage"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { customElement } from "@lit/reactive-element/decorators/custom-element.js"; | ||||
| import { CSSResult, TemplateResult, html } from "lit"; | ||||
|  | ||||
| import AKGlobal from "@goauthentik/common/styles/authentik.css"; | ||||
| import PFButton from "@patternfly/patternfly/components/Button/button.css"; | ||||
| import PFForm from "@patternfly/patternfly/components/Form/form.css"; | ||||
| import PFRadio from "@patternfly/patternfly/components/Radio/radio.css"; | ||||
| import PFBase from "@patternfly/patternfly/patternfly-base.css"; | ||||
|  | ||||
| @customElement("ak-application-wizard-type-oauth-api") | ||||
| export class TypeOAuthAPIApplicationWizardPage extends WizardPage { | ||||
|     static get styles(): CSSResult[] { | ||||
|         return [PFBase, PFButton, PFForm, PFRadio, AKGlobal]; | ||||
|     } | ||||
|  | ||||
|     sidebarLabel = () => t`Method details`; | ||||
|  | ||||
|     render(): TemplateResult { | ||||
|         return html`<form class="pf-c-form pf-m-horizontal"> | ||||
|             <p> | ||||
|                 ${t`This configuration can be used to authenticate to authentik with other APIs other otherwise programmatically.`} | ||||
|             </p> | ||||
|             <p> | ||||
|                 ${t`By default, all service accounts can authenticate as this application, as long as they have a valid token of the type app-password.`} | ||||
|             </p> | ||||
|         </form> `; | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,78 @@ | ||||
| import "@goauthentik/elements/forms/HorizontalFormElement"; | ||||
| import { WizardPage } from "@goauthentik/elements/wizard/WizardPage"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { customElement } from "@lit/reactive-element/decorators/custom-element.js"; | ||||
| import { CSSResult, TemplateResult, html } from "lit"; | ||||
|  | ||||
| import AKGlobal from "@goauthentik/common/styles/authentik.css"; | ||||
| import PFButton from "@patternfly/patternfly/components/Button/button.css"; | ||||
| import PFForm from "@patternfly/patternfly/components/Form/form.css"; | ||||
| import PFRadio from "@patternfly/patternfly/components/Radio/radio.css"; | ||||
| import PFBase from "@patternfly/patternfly/patternfly-base.css"; | ||||
|  | ||||
| import { TypeCreate } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-application-wizard-type-oauth") | ||||
| export class TypeOAuthApplicationWizardPage extends WizardPage { | ||||
|     applicationTypes: TypeCreate[] = [ | ||||
|         { | ||||
|             component: "ak-application-wizard-type-oauth-code", | ||||
|             name: t`Web application`, | ||||
|             description: t`Applications which handle the authentication server-side (for example, Python, Go, Rust, Java, PHP)`, | ||||
|             modelName: "", | ||||
|         }, | ||||
|         { | ||||
|             component: "ak-application-wizard-type-oauth-implicit", | ||||
|             name: t`Single-page applications`, | ||||
|             description: t`Single-page applications which handle authentication in the browser (for example, Javascript, Angular, React, Vue)`, | ||||
|             modelName: "", | ||||
|         }, | ||||
|         { | ||||
|             component: "ak-application-wizard-type-oauth-implicit", | ||||
|             name: t`Native application`, | ||||
|             description: t`Applications which redirect users to a non-web callback (for example, Android, iOS)`, | ||||
|             modelName: "", | ||||
|         }, | ||||
|         { | ||||
|             component: "ak-application-wizard-type-oauth-api", | ||||
|             name: t`API`, | ||||
|             description: t`Authentication without user interaction, or machine-to-machine authentication.`, | ||||
|             modelName: "", | ||||
|         }, | ||||
|     ]; | ||||
|  | ||||
|     static get styles(): CSSResult[] { | ||||
|         return [PFBase, PFButton, PFForm, PFRadio, AKGlobal]; | ||||
|     } | ||||
|  | ||||
|     sidebarLabel = () => t`Application type`; | ||||
|  | ||||
|     render(): TemplateResult { | ||||
|         return html`<form class="pf-c-form pf-m-horizontal"> | ||||
|             ${this.applicationTypes.map((type) => { | ||||
|                 return html`<div class="pf-c-radio"> | ||||
|                     <input | ||||
|                         class="pf-c-radio__input" | ||||
|                         type="radio" | ||||
|                         name="type" | ||||
|                         id=${type.component} | ||||
|                         @change=${() => { | ||||
|                             this.host.steps = [ | ||||
|                                 "ak-application-wizard-initial", | ||||
|                                 "ak-application-wizard-type", | ||||
|                                 "ak-application-wizard-type-oauth", | ||||
|                                 type.component, | ||||
|                             ]; | ||||
|                             this.host.state["oauth-type"] = type.component; | ||||
|                             this.host.isValid = true; | ||||
|                         }} | ||||
|                     /> | ||||
|                     <label class="pf-c-radio__label" for=${type.component}>${type.name}</label> | ||||
|                     <span class="pf-c-radio__description">${type.description}</span> | ||||
|                 </div>`; | ||||
|             })} | ||||
|         </form> `; | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,72 @@ | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { KeyUnknown } from "@goauthentik/elements/forms/Form"; | ||||
| import "@goauthentik/elements/forms/HorizontalFormElement"; | ||||
| import { WizardFormPage } from "@goauthentik/elements/wizard/WizardFormPage"; | ||||
| import "@goauthentik/elements/wizard/WizardFormPage"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { customElement } from "@lit/reactive-element/decorators/custom-element.js"; | ||||
| import { TemplateResult, html } from "lit"; | ||||
| import { ifDefined } from "lit/directives/if-defined.js"; | ||||
| import { until } from "lit/directives/until.js"; | ||||
|  | ||||
| import { | ||||
|     ClientTypeEnum, | ||||
|     FlowsApi, | ||||
|     FlowsInstancesListDesignationEnum, | ||||
|     OAuth2ProviderRequest, | ||||
|     ProvidersApi, | ||||
| } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-application-wizard-type-oauth-code") | ||||
| export class TypeOAuthCodeApplicationWizardPage extends WizardFormPage { | ||||
|     sidebarLabel = () => t`Method details`; | ||||
|  | ||||
|     nextDataCallback = async (data: KeyUnknown): Promise<boolean> => { | ||||
|         this.host.addActionBefore(t`Create provider`, async (): Promise<boolean> => { | ||||
|             const req: OAuth2ProviderRequest = { | ||||
|                 name: this.host.state["name"] as string, | ||||
|                 clientType: ClientTypeEnum.Confidential, | ||||
|                 authorizationFlow: data.authorizationFlow as string, | ||||
|             }; | ||||
|             const provider = await new ProvidersApi(DEFAULT_CONFIG).providersOauth2Create({ | ||||
|                 oAuth2ProviderRequest: req, | ||||
|             }); | ||||
|             this.host.state["provider"] = provider; | ||||
|             return true; | ||||
|         }); | ||||
|         return true; | ||||
|     }; | ||||
|  | ||||
|     renderForm(): TemplateResult { | ||||
|         return html`<form class="pf-c-form pf-m-horizontal"> | ||||
|             <ak-form-element-horizontal | ||||
|                 label=${t`Authorization flow`} | ||||
|                 ?required=${true} | ||||
|                 name="authorizationFlow" | ||||
|             > | ||||
|                 <select class="pf-c-form-control"> | ||||
|                     ${until( | ||||
|                         new FlowsApi(DEFAULT_CONFIG) | ||||
|                             .flowsInstancesList({ | ||||
|                                 ordering: "slug", | ||||
|                                 designation: FlowsInstancesListDesignationEnum.Authorization, | ||||
|                             }) | ||||
|                             .then((flows) => { | ||||
|                                 return flows.results.map((flow) => { | ||||
|                                     return html`<option value=${ifDefined(flow.pk)}> | ||||
|                                         ${flow.name} (${flow.slug}) | ||||
|                                     </option>`; | ||||
|                                 }); | ||||
|                             }), | ||||
|                         html`<option>${t`Loading...`}</option>`, | ||||
|                     )} | ||||
|                 </select> | ||||
|                 <p class="pf-c-form__helper-text"> | ||||
|                     ${t`Flow used when users access this application.`} | ||||
|                 </p> | ||||
|             </ak-form-element-horizontal> | ||||
|         </form>`; | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,16 @@ | ||||
| import "@goauthentik/elements/forms/HorizontalFormElement"; | ||||
| import { WizardFormPage } from "@goauthentik/elements/wizard/WizardFormPage"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { customElement } from "@lit/reactive-element/decorators/custom-element.js"; | ||||
| import { TemplateResult, html } from "lit"; | ||||
|  | ||||
| @customElement("ak-application-wizard-type-oauth-implicit") | ||||
| export class TypeOAuthImplicitApplicationWizardPage extends WizardFormPage { | ||||
|     sidebarLabel = () => t`Method details`; | ||||
|  | ||||
|     render(): TemplateResult { | ||||
|         return html`<form class="pf-c-form pf-m-horizontal">some stuff idk</form> `; | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,65 @@ | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { KeyUnknown } from "@goauthentik/elements/forms/Form"; | ||||
| import "@goauthentik/elements/forms/HorizontalFormElement"; | ||||
| import { WizardFormPage } from "@goauthentik/elements/wizard/WizardFormPage"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { customElement } from "@lit/reactive-element/decorators/custom-element.js"; | ||||
| import { TemplateResult, html } from "lit"; | ||||
|  | ||||
| import { | ||||
|     FlowDesignationEnum, | ||||
|     FlowsApi, | ||||
|     ProvidersApi, | ||||
|     ProxyProviderRequest, | ||||
| } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-application-wizard-type-proxy") | ||||
| export class TypeProxyApplicationWizardPage extends WizardFormPage { | ||||
|     sidebarLabel = () => t`Proxy details`; | ||||
|  | ||||
|     nextDataCallback = async (data: KeyUnknown): Promise<boolean> => { | ||||
|         let name = this.host.state["name"] as string; | ||||
|         // Check if a provider with the name already exists | ||||
|         const providers = await new ProvidersApi(DEFAULT_CONFIG).providersAllList({ | ||||
|             search: name, | ||||
|         }); | ||||
|         if (providers.results.filter((provider) => provider.name == name)) { | ||||
|             name += "-1"; | ||||
|         } | ||||
|         this.host.addActionBefore(t`Create provider`, async (): Promise<boolean> => { | ||||
|             // Get all flows and default to the implicit authorization | ||||
|             const flows = await new FlowsApi(DEFAULT_CONFIG).flowsInstancesList({ | ||||
|                 designation: FlowDesignationEnum.Authorization, | ||||
|                 ordering: "slug", | ||||
|             }); | ||||
|             const req: ProxyProviderRequest = { | ||||
|                 name: name, | ||||
|                 authorizationFlow: flows.results[0].pk, | ||||
|                 externalHost: data.externalHost as string, | ||||
|             }; | ||||
|             const provider = await new ProvidersApi(DEFAULT_CONFIG).providersProxyCreate({ | ||||
|                 proxyProviderRequest: req, | ||||
|             }); | ||||
|             this.host.state["provider"] = provider; | ||||
|             return true; | ||||
|         }); | ||||
|         return true; | ||||
|     }; | ||||
|  | ||||
|     renderForm(): TemplateResult { | ||||
|         return html`<form class="pf-c-form pf-m-horizontal"> | ||||
|             <ak-form-element-horizontal | ||||
|                 label=${t`External domain`} | ||||
|                 name="externalHost" | ||||
|                 ?required=${true} | ||||
|             > | ||||
|                 <input type="text" value="" class="pf-c-form-control" required /> | ||||
|                 <p class="pf-c-form__helper-text"> | ||||
|                     ${t`External domain you will be accessing the domain from.`} | ||||
|                 </p> | ||||
|             </ak-form-element-horizontal> | ||||
|         </form> `; | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,66 @@ | ||||
| import "@goauthentik/elements/forms/HorizontalFormElement"; | ||||
| import { WizardPage } from "@goauthentik/elements/wizard/WizardPage"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { customElement } from "@lit/reactive-element/decorators/custom-element.js"; | ||||
| import { CSSResult, TemplateResult, html } from "lit"; | ||||
|  | ||||
| import AKGlobal from "@goauthentik/common/styles/authentik.css"; | ||||
| import PFButton from "@patternfly/patternfly/components/Button/button.css"; | ||||
| import PFForm from "@patternfly/patternfly/components/Form/form.css"; | ||||
| import PFRadio from "@patternfly/patternfly/components/Radio/radio.css"; | ||||
| import PFBase from "@patternfly/patternfly/patternfly-base.css"; | ||||
|  | ||||
| import { TypeCreate } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-application-wizard-type-saml") | ||||
| export class TypeOAuthApplicationWizardPage extends WizardPage { | ||||
|     applicationTypes: TypeCreate[] = [ | ||||
|         { | ||||
|             component: "ak-application-wizard-type-saml-import", | ||||
|             name: t`Import SAML Metadata`, | ||||
|             description: t`Import the metadata document of the applicaation you want to configure.`, | ||||
|             modelName: "", | ||||
|         }, | ||||
|         { | ||||
|             component: "ak-application-wizard-type-saml-config", | ||||
|             name: t`Manual configuration`, | ||||
|             description: t`Manually configure SAML`, | ||||
|             modelName: "", | ||||
|         }, | ||||
|     ]; | ||||
|  | ||||
|     static get styles(): CSSResult[] { | ||||
|         return [PFBase, PFButton, PFForm, PFRadio, AKGlobal]; | ||||
|     } | ||||
|  | ||||
|     sidebarLabel = () => t`Application type`; | ||||
|  | ||||
|     render(): TemplateResult { | ||||
|         return html`<form class="pf-c-form pf-m-horizontal"> | ||||
|             ${this.applicationTypes.map((type) => { | ||||
|                 return html`<div class="pf-c-radio"> | ||||
|                     <input | ||||
|                         class="pf-c-radio__input" | ||||
|                         type="radio" | ||||
|                         name="type" | ||||
|                         id=${type.component} | ||||
|                         @change=${() => { | ||||
|                             this.host.steps = [ | ||||
|                                 "ak-application-wizard-initial", | ||||
|                                 "ak-application-wizard-type", | ||||
|                                 "ak-application-wizard-type-saml", | ||||
|                                 type.component, | ||||
|                             ]; | ||||
|                             this.host.state["saml-type"] = type.component; | ||||
|                             this.host.isValid = true; | ||||
|                         }} | ||||
|                     /> | ||||
|                     <label class="pf-c-radio__label" for=${type.component}>${type.name}</label> | ||||
|                     <span class="pf-c-radio__description">${type.description}</span> | ||||
|                 </div>`; | ||||
|             })} | ||||
|         </form> `; | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,56 @@ | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { KeyUnknown } from "@goauthentik/elements/forms/Form"; | ||||
| import "@goauthentik/elements/forms/HorizontalFormElement"; | ||||
| import { WizardFormPage } from "@goauthentik/elements/wizard/WizardFormPage"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { customElement } from "@lit/reactive-element/decorators/custom-element.js"; | ||||
| import { TemplateResult, html } from "lit"; | ||||
|  | ||||
| import { FlowDesignationEnum, FlowsApi, ProvidersApi, SAMLProviderRequest } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-application-wizard-type-saml-config") | ||||
| export class TypeSAMLApplicationWizardPage extends WizardFormPage { | ||||
|     sidebarLabel = () => t`SAML details`; | ||||
|  | ||||
|     nextDataCallback = async (data: KeyUnknown): Promise<boolean> => { | ||||
|         let name = this.host.state["name"] as string; | ||||
|         // Check if a provider with the name already exists | ||||
|         const providers = await new ProvidersApi(DEFAULT_CONFIG).providersAllList({ | ||||
|             search: name, | ||||
|         }); | ||||
|         if (providers.results.filter((provider) => provider.name == name)) { | ||||
|             name += "-1"; | ||||
|         } | ||||
|         this.host.addActionBefore(t`Create provider`, async (): Promise<boolean> => { | ||||
|             // Get all flows and default to the implicit authorization | ||||
|             const flows = await new FlowsApi(DEFAULT_CONFIG).flowsInstancesList({ | ||||
|                 designation: FlowDesignationEnum.Authorization, | ||||
|                 ordering: "slug", | ||||
|             }); | ||||
|             const req: SAMLProviderRequest = { | ||||
|                 name: name, | ||||
|                 authorizationFlow: flows.results[0].pk, | ||||
|                 acsUrl: data.acsUrl as string, | ||||
|             }; | ||||
|             const provider = await new ProvidersApi(DEFAULT_CONFIG).providersSamlCreate({ | ||||
|                 sAMLProviderRequest: req, | ||||
|             }); | ||||
|             this.host.state["provider"] = provider; | ||||
|             return true; | ||||
|         }); | ||||
|         return true; | ||||
|     }; | ||||
|  | ||||
|     renderForm(): TemplateResult { | ||||
|         return html`<form class="pf-c-form pf-m-horizontal"> | ||||
|             <ak-form-element-horizontal label=${t`ACS URL`} name="acsUrl" ?required=${true}> | ||||
|                 <input type="text" value="" class="pf-c-form-control" required /> | ||||
|                 <p class="pf-c-form__helper-text"> | ||||
|                     ${t`URL that authentik will redirect back to after successful authentication.`} | ||||
|                 </p> | ||||
|             </ak-form-element-horizontal> | ||||
|         </form> `; | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,58 @@ | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { KeyUnknown } from "@goauthentik/elements/forms/Form"; | ||||
| import "@goauthentik/elements/forms/HorizontalFormElement"; | ||||
| import { WizardFormPage } from "@goauthentik/elements/wizard/WizardFormPage"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { customElement } from "@lit/reactive-element/decorators/custom-element.js"; | ||||
| import { TemplateResult, html } from "lit"; | ||||
|  | ||||
| import { | ||||
|     FlowDesignationEnum, | ||||
|     FlowsApi, | ||||
|     ProvidersApi, | ||||
|     ProvidersSamlImportMetadataCreateRequest, | ||||
| } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-application-wizard-type-saml-import") | ||||
| export class TypeSAMLImportApplicationWizardPage extends WizardFormPage { | ||||
|     sidebarLabel = () => t`Import SAML metadata`; | ||||
|  | ||||
|     nextDataCallback = async (data: KeyUnknown): Promise<boolean> => { | ||||
|         let name = this.host.state["name"] as string; | ||||
|         // Check if a provider with the name already exists | ||||
|         const providers = await new ProvidersApi(DEFAULT_CONFIG).providersAllList({ | ||||
|             search: name, | ||||
|         }); | ||||
|         if (providers.results.filter((provider) => provider.name == name)) { | ||||
|             name += "-1"; | ||||
|         } | ||||
|         this.host.addActionBefore(t`Create provider`, async (): Promise<boolean> => { | ||||
|             // Get all flows and default to the implicit authorization | ||||
|             const flows = await new FlowsApi(DEFAULT_CONFIG).flowsInstancesList({ | ||||
|                 designation: FlowDesignationEnum.Authorization, | ||||
|                 ordering: "slug", | ||||
|             }); | ||||
|             const req: ProvidersSamlImportMetadataCreateRequest = { | ||||
|                 name: name, | ||||
|                 authorizationFlow: flows.results[0].slug, | ||||
|                 file: data["metadata"] as Blob, | ||||
|             }; | ||||
|             const provider = await new ProvidersApi( | ||||
|                 DEFAULT_CONFIG, | ||||
|             ).providersSamlImportMetadataCreate(req); | ||||
|             this.host.state["provider"] = provider; | ||||
|             return true; | ||||
|         }); | ||||
|         return true; | ||||
|     }; | ||||
|  | ||||
|     renderForm(): TemplateResult { | ||||
|         return html`<form class="pf-c-form pf-m-horizontal"> | ||||
|             <ak-form-element-horizontal label=${t`Metadata`} name="metadata"> | ||||
|                 <input type="file" value="" class="pf-c-form-control" /> | ||||
|             </ak-form-element-horizontal> | ||||
|         </form> `; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										106
									
								
								web/src/admin/blueprints/BlueprintForm.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										106
									
								
								web/src/admin/blueprints/BlueprintForm.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,106 @@ | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { first } from "@goauthentik/common/utils"; | ||||
| import "@goauthentik/elements/CodeMirror"; | ||||
| import "@goauthentik/elements/forms/FormGroup"; | ||||
| import "@goauthentik/elements/forms/HorizontalFormElement"; | ||||
| import { ModelForm } from "@goauthentik/elements/forms/ModelForm"; | ||||
| import YAML from "yaml"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { TemplateResult, html } from "lit"; | ||||
| import { customElement } from "lit/decorators.js"; | ||||
| import { ifDefined } from "lit/directives/if-defined.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="${ifDefined(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, true)} | ||||
|                     /> | ||||
|                     <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>`; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										147
									
								
								web/src/admin/blueprints/BlueprintListPage.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										147
									
								
								web/src/admin/blueprints/BlueprintListPage.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,147 @@ | ||||
| import "@goauthentik/admin/blueprints/BlueprintForm"; | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { EVENT_REFRESH } from "@goauthentik/common/constants"; | ||||
| import { uiConfig } from "@goauthentik/common/ui/config"; | ||||
| import { PFColor } from "@goauthentik/elements/Label"; | ||||
| import "@goauthentik/elements/buttons/ActionButton"; | ||||
| import "@goauthentik/elements/buttons/SpinnerButton"; | ||||
| import "@goauthentik/elements/forms/DeleteBulkForm"; | ||||
| import "@goauthentik/elements/forms/ModalForm"; | ||||
| import { PaginatedResponse } from "@goauthentik/elements/table/Table"; | ||||
| import { TableColumn } from "@goauthentik/elements/table/Table"; | ||||
| import { TablePage } from "@goauthentik/elements/table/TablePage"; | ||||
|  | ||||
| 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.Warning: | ||||
|             return t`Warning`; | ||||
|         case BlueprintInstanceStatusEnum.Error: | ||||
|             return t`Error`; | ||||
|     } | ||||
|     return t`Unknown`; | ||||
| } | ||||
| @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<PaginatedResponse<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> | ||||
|         `; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										44
									
								
								web/src/admin/crypto/CertificateGenerateForm.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								web/src/admin/crypto/CertificateGenerateForm.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,44 @@ | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { Form } from "@goauthentik/elements/forms/Form"; | ||||
| import "@goauthentik/elements/forms/HorizontalFormElement"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { TemplateResult, html } from "lit"; | ||||
| import { customElement } from "lit/decorators.js"; | ||||
|  | ||||
| import { CertificateGenerationRequest, CertificateKeyPair, CryptoApi } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-crypto-certificate-generate-form") | ||||
| export class CertificateKeyPairForm extends Form<CertificateGenerationRequest> { | ||||
|     getSuccessMessage(): string { | ||||
|         return t`Successfully generated certificate-key pair.`; | ||||
|     } | ||||
|  | ||||
|     send = (data: CertificateGenerationRequest): Promise<CertificateKeyPair> => { | ||||
|         return new CryptoApi(DEFAULT_CONFIG).cryptoCertificatekeypairsGenerateCreate({ | ||||
|             certificateGenerationRequest: data, | ||||
|         }); | ||||
|     }; | ||||
|  | ||||
|     renderForm(): TemplateResult { | ||||
|         return html`<form class="pf-c-form pf-m-horizontal"> | ||||
|             <ak-form-element-horizontal label=${t`Common Name`} name="commonName" ?required=${true}> | ||||
|                 <input type="text" class="pf-c-form-control" required /> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-element-horizontal label=${t`Subject-alt name`} name="subjectAltName"> | ||||
|                 <input class="pf-c-form-control" type="text" /> | ||||
|                 <p class="pf-c-form__helper-text"> | ||||
|                     ${t`Optional, comma-separated SubjectAlt Names.`} | ||||
|                 </p> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-element-horizontal | ||||
|                 label=${t`Validity days`} | ||||
|                 name="validityDays" | ||||
|                 ?required=${true} | ||||
|             > | ||||
|                 <input class="pf-c-form-control" type="number" value="365" /> | ||||
|             </ak-form-element-horizontal> | ||||
|         </form>`; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										74
									
								
								web/src/admin/crypto/CertificateKeyPairForm.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								web/src/admin/crypto/CertificateKeyPairForm.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,74 @@ | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import "@goauthentik/elements/CodeMirror"; | ||||
| import "@goauthentik/elements/forms/HorizontalFormElement"; | ||||
| import { ModelForm } from "@goauthentik/elements/forms/ModelForm"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { TemplateResult, html } from "lit"; | ||||
| import { customElement } from "lit/decorators.js"; | ||||
| import { ifDefined } from "lit/directives/if-defined.js"; | ||||
|  | ||||
| import { CertificateKeyPair, CertificateKeyPairRequest, CryptoApi } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-crypto-certificate-form") | ||||
| export class CertificateKeyPairForm extends ModelForm<CertificateKeyPair, string> { | ||||
|     loadInstance(pk: string): Promise<CertificateKeyPair> { | ||||
|         return new CryptoApi(DEFAULT_CONFIG).cryptoCertificatekeypairsRetrieve({ | ||||
|             kpUuid: pk, | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     getSuccessMessage(): string { | ||||
|         if (this.instance) { | ||||
|             return t`Successfully updated certificate-key pair.`; | ||||
|         } else { | ||||
|             return t`Successfully created certificate-key pair.`; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     send = (data: CertificateKeyPair): Promise<CertificateKeyPair> => { | ||||
|         if (this.instance) { | ||||
|             return new CryptoApi(DEFAULT_CONFIG).cryptoCertificatekeypairsPartialUpdate({ | ||||
|                 kpUuid: this.instance.pk || "", | ||||
|                 patchedCertificateKeyPairRequest: data, | ||||
|             }); | ||||
|         } else { | ||||
|             return new CryptoApi(DEFAULT_CONFIG).cryptoCertificatekeypairsCreate({ | ||||
|                 certificateKeyPairRequest: data as unknown as CertificateKeyPairRequest, | ||||
|             }); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     renderForm(): TemplateResult { | ||||
|         return html`<form class="pf-c-form pf-m-horizontal"> | ||||
|             <ak-form-element-horizontal label=${t`Name`} name="name" ?required=${true}> | ||||
|                 <input | ||||
|                     type="text" | ||||
|                     value="${ifDefined(this.instance?.name)}" | ||||
|                     class="pf-c-form-control" | ||||
|                     required | ||||
|                 /> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-element-horizontal | ||||
|                 label=${t`Certificate`} | ||||
|                 name="certificateData" | ||||
|                 ?writeOnly=${this.instance !== undefined} | ||||
|                 ?required=${true} | ||||
|             > | ||||
|                 <textarea class="pf-c-form-control" required></textarea> | ||||
|                 <p class="pf-c-form__helper-text">${t`PEM-encoded Certificate data.`}</p> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-element-horizontal | ||||
|                 name="keyData" | ||||
|                 ?writeOnly=${this.instance !== undefined} | ||||
|                 label=${t`Private Key`} | ||||
|             > | ||||
|                 <textarea class="pf-c-form-control"></textarea> | ||||
|                 <p class="pf-c-form__helper-text"> | ||||
|                     ${t`Optional Private Key. If this is set, you can use this keypair for encryption.`} | ||||
|                 </p> | ||||
|             </ak-form-element-horizontal> | ||||
|         </form>`; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										206
									
								
								web/src/admin/crypto/CertificateKeyPairListPage.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										206
									
								
								web/src/admin/crypto/CertificateKeyPairListPage.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,206 @@ | ||||
| import "@goauthentik/admin/crypto/CertificateGenerateForm"; | ||||
| import "@goauthentik/admin/crypto/CertificateKeyPairForm"; | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { uiConfig } from "@goauthentik/common/ui/config"; | ||||
| import { PFColor } from "@goauthentik/elements/Label"; | ||||
| import "@goauthentik/elements/buttons/SpinnerButton"; | ||||
| import "@goauthentik/elements/forms/DeleteBulkForm"; | ||||
| import "@goauthentik/elements/forms/ModalForm"; | ||||
| import { PaginatedResponse } from "@goauthentik/elements/table/Table"; | ||||
| import { TableColumn } from "@goauthentik/elements/table/Table"; | ||||
| import { TablePage } from "@goauthentik/elements/table/TablePage"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { CSSResult, TemplateResult, html } from "lit"; | ||||
| import { customElement, property } from "lit/decorators.js"; | ||||
|  | ||||
| import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css"; | ||||
|  | ||||
| import { CertificateKeyPair, CryptoApi } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-crypto-certificate-list") | ||||
| export class CertificateKeyPairListPage extends TablePage<CertificateKeyPair> { | ||||
|     expandable = true; | ||||
|     checkbox = true; | ||||
|  | ||||
|     searchEnabled(): boolean { | ||||
|         return true; | ||||
|     } | ||||
|     pageTitle(): string { | ||||
|         return t`Certificate-Key Pairs`; | ||||
|     } | ||||
|     pageDescription(): string { | ||||
|         return t`Import certificates of external providers or create certificates to sign requests with.`; | ||||
|     } | ||||
|     pageIcon(): string { | ||||
|         return "pf-icon pf-icon-key"; | ||||
|     } | ||||
|  | ||||
|     @property() | ||||
|     order = "name"; | ||||
|  | ||||
|     static get styles(): CSSResult[] { | ||||
|         return super.styles.concat(PFDescriptionList); | ||||
|     } | ||||
|  | ||||
|     async apiEndpoint(page: number): Promise<PaginatedResponse<CertificateKeyPair>> { | ||||
|         return new CryptoApi(DEFAULT_CONFIG).cryptoCertificatekeypairsList({ | ||||
|             ordering: this.order, | ||||
|             page: page, | ||||
|             pageSize: (await uiConfig()).pagination.perPage, | ||||
|             search: this.search || "", | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     columns(): TableColumn[] { | ||||
|         return [ | ||||
|             new TableColumn(t`Name`, "name"), | ||||
|             new TableColumn(t`Private key available?`), | ||||
|             new TableColumn(t`Expiry date`), | ||||
|             new TableColumn(t`Actions`), | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     renderToolbarSelected(): TemplateResult { | ||||
|         const disabled = this.selectedElements.length < 1; | ||||
|         return html`<ak-forms-delete-bulk | ||||
|             objectLabel=${t`Certificate-Key Pair(s)`} | ||||
|             .objects=${this.selectedElements} | ||||
|             .metadata=${(item: CertificateKeyPair) => { | ||||
|                 return [ | ||||
|                     { key: t`Name`, value: item.name }, | ||||
|                     { key: t`Expiry`, value: item.certExpiry.toLocaleString() }, | ||||
|                 ]; | ||||
|             }} | ||||
|             .usedBy=${(item: CertificateKeyPair) => { | ||||
|                 return new CryptoApi(DEFAULT_CONFIG).cryptoCertificatekeypairsUsedByList({ | ||||
|                     kpUuid: item.pk, | ||||
|                 }); | ||||
|             }} | ||||
|             .delete=${(item: CertificateKeyPair) => { | ||||
|                 return new CryptoApi(DEFAULT_CONFIG).cryptoCertificatekeypairsDestroy({ | ||||
|                     kpUuid: item.pk, | ||||
|                 }); | ||||
|             }} | ||||
|         > | ||||
|             <button ?disabled=${disabled} slot="trigger" class="pf-c-button pf-m-danger"> | ||||
|                 ${t`Delete`} | ||||
|             </button> | ||||
|         </ak-forms-delete-bulk>`; | ||||
|     } | ||||
|  | ||||
|     row(item: CertificateKeyPair): TemplateResult[] { | ||||
|         let managedSubText = t`Managed by authentik`; | ||||
|         if (item.managed && item.managed.startsWith("goauthentik.io/crypto/discovered")) { | ||||
|             managedSubText = t`Managed by authentik (Discovered)`; | ||||
|         } | ||||
|         return [ | ||||
|             html`<div>${item.name}</div> | ||||
|                 ${item.managed ? html`<small>${managedSubText}</small>` : html``}`, | ||||
|             html`<ak-label color=${item.privateKeyAvailable ? PFColor.Green : PFColor.Grey}> | ||||
|                 ${item.privateKeyAvailable ? t`Yes (${item.privateKeyType?.toUpperCase()})` : t`No`} | ||||
|             </ak-label>`, | ||||
|             html`<ak-label color=${item.certExpiry > new Date() ? PFColor.Green : PFColor.Orange}> | ||||
|                 ${item.certExpiry?.toLocaleString()} | ||||
|             </ak-label>`, | ||||
|             html`<ak-forms-modal> | ||||
|                 <span slot="submit"> ${t`Update`} </span> | ||||
|                 <span slot="header"> ${t`Update Certificate-Key Pair`} </span> | ||||
|                 <ak-crypto-certificate-form slot="form" .instancePk=${item.pk}> | ||||
|                 </ak-crypto-certificate-form> | ||||
|                 <button slot="trigger" class="pf-c-button pf-m-plain"> | ||||
|                     <i class="fas fa-edit"></i> | ||||
|                 </button> | ||||
|             </ak-forms-modal>`, | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     renderExpanded(item: CertificateKeyPair): TemplateResult { | ||||
|         return html`<td role="cell" colspan="4"> | ||||
|                 <div class="pf-c-table__expandable-row-content"> | ||||
|                     <dl class="pf-c-description-list pf-m-horizontal"> | ||||
|                         <div class="pf-c-description-list__group"> | ||||
|                             <dt class="pf-c-description-list__term"> | ||||
|                                 <span class="pf-c-description-list__text" | ||||
|                                     >${t`Certificate Fingerprint (SHA1)`}</span | ||||
|                                 > | ||||
|                             </dt> | ||||
|                             <dd class="pf-c-description-list__description"> | ||||
|                                 <div class="pf-c-description-list__text"> | ||||
|                                     ${item.fingerprintSha1} | ||||
|                                 </div> | ||||
|                             </dd> | ||||
|                         </div> | ||||
|                         <div class="pf-c-description-list__group"> | ||||
|                             <dt class="pf-c-description-list__term"> | ||||
|                                 <span class="pf-c-description-list__text" | ||||
|                                     >${t`Certificate Fingerprint (SHA256)`}</span | ||||
|                                 > | ||||
|                             </dt> | ||||
|                             <dd class="pf-c-description-list__description"> | ||||
|                                 <div class="pf-c-description-list__text"> | ||||
|                                     ${item.fingerprintSha256} | ||||
|                                 </div> | ||||
|                             </dd> | ||||
|                         </div> | ||||
|                         <div class="pf-c-description-list__group"> | ||||
|                             <dt class="pf-c-description-list__term"> | ||||
|                                 <span class="pf-c-description-list__text" | ||||
|                                     >${t`Certificate Subject`}</span | ||||
|                                 > | ||||
|                             </dt> | ||||
|                             <dd class="pf-c-description-list__description"> | ||||
|                                 <div class="pf-c-description-list__text">${item.certSubject}</div> | ||||
|                             </dd> | ||||
|                         </div> | ||||
|                         <div class="pf-c-description-list__group"> | ||||
|                             <dt class="pf-c-description-list__term"> | ||||
|                                 <span class="pf-c-description-list__text">${t`Download`}</span> | ||||
|                             </dt> | ||||
|                             <dd class="pf-c-description-list__description"> | ||||
|                                 <div class="pf-c-description-list__text"> | ||||
|                                     <a | ||||
|                                         class="pf-c-button pf-m-secondary" | ||||
|                                         target="_blank" | ||||
|                                         href=${item.certificateDownloadUrl} | ||||
|                                     > | ||||
|                                         ${t`Download Certificate`} | ||||
|                                     </a> | ||||
|                                     ${item.privateKeyAvailable | ||||
|                                         ? html`<a | ||||
|                                               class="pf-c-button pf-m-secondary" | ||||
|                                               target="_blank" | ||||
|                                               href=${item.privateKeyDownloadUrl} | ||||
|                                           > | ||||
|                                               ${t`Download Private key`} | ||||
|                                           </a>` | ||||
|                                         : html``} | ||||
|                                 </div> | ||||
|                             </dd> | ||||
|                         </div> | ||||
|                     </dl> | ||||
|                 </div> | ||||
|             </td> | ||||
|             <td></td> | ||||
|             <td></td>`; | ||||
|     } | ||||
|  | ||||
|     renderObjectCreate(): TemplateResult { | ||||
|         return html` | ||||
|             <ak-forms-modal> | ||||
|                 <span slot="submit"> ${t`Create`} </span> | ||||
|                 <span slot="header"> ${t`Create Certificate-Key Pair`} </span> | ||||
|                 <ak-crypto-certificate-form slot="form"> </ak-crypto-certificate-form> | ||||
|                 <button slot="trigger" class="pf-c-button pf-m-primary">${t`Create`}</button> | ||||
|             </ak-forms-modal> | ||||
|             <ak-forms-modal> | ||||
|                 <span slot="submit"> ${t`Generate`} </span> | ||||
|                 <span slot="header"> ${t`Generate Certificate-Key Pair`} </span> | ||||
|                 <ak-crypto-certificate-generate-form slot="form"> | ||||
|                 </ak-crypto-certificate-generate-form> | ||||
|                 <button slot="trigger" class="pf-c-button pf-m-secondary">${t`Generate`}</button> | ||||
|             </ak-forms-modal> | ||||
|         `; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										399
									
								
								web/src/admin/events/EventInfo.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										399
									
								
								web/src/admin/events/EventInfo.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,399 @@ | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { VERSION } from "@goauthentik/common/constants"; | ||||
| import { EventContext, EventModel, EventWithContext } from "@goauthentik/common/events"; | ||||
| import { AKElement } from "@goauthentik/elements/Base"; | ||||
| import "@goauthentik/elements/Expand"; | ||||
| import "@goauthentik/elements/Spinner"; | ||||
| import { PFSize } from "@goauthentik/elements/Spinner"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { CSSResult, TemplateResult, css, html } from "lit"; | ||||
| import { customElement, property } from "lit/decorators.js"; | ||||
| import { until } from "lit/directives/until.js"; | ||||
|  | ||||
| import PFButton from "@patternfly/patternfly/components/Button/button.css"; | ||||
| import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css"; | ||||
| import PFList from "@patternfly/patternfly/components/List/list.css"; | ||||
| import PFFlex from "@patternfly/patternfly/layouts/Flex/flex.css"; | ||||
| import PFBase from "@patternfly/patternfly/patternfly-base.css"; | ||||
|  | ||||
| import { EventActions, FlowsApi } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-event-info") | ||||
| export class EventInfo extends AKElement { | ||||
|     @property({ attribute: false }) | ||||
|     event!: EventWithContext; | ||||
|  | ||||
|     static get styles(): CSSResult[] { | ||||
|         return [ | ||||
|             PFBase, | ||||
|             PFButton, | ||||
|             PFFlex, | ||||
|             PFList, | ||||
|             PFDescriptionList, | ||||
|             css` | ||||
|                 code { | ||||
|                     display: block; | ||||
|                     white-space: pre-wrap; | ||||
|                 } | ||||
|                 .pf-l-flex { | ||||
|                     justify-content: space-between; | ||||
|                 } | ||||
|                 .pf-l-flex__item { | ||||
|                     min-width: 25%; | ||||
|                 } | ||||
|                 iframe { | ||||
|                     width: 100%; | ||||
|                     height: 50rem; | ||||
|                 } | ||||
|             `, | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     getModelInfo(context: EventModel): TemplateResult { | ||||
|         if (context === null) { | ||||
|             return html`<span>-</span>`; | ||||
|         } | ||||
|         return html`<dl class="pf-c-description-list pf-m-horizontal"> | ||||
|             <div class="pf-c-description-list__group"> | ||||
|                 <dt class="pf-c-description-list__term"> | ||||
|                     <span class="pf-c-description-list__text">${t`UID`}</span> | ||||
|                 </dt> | ||||
|                 <dd class="pf-c-description-list__description"> | ||||
|                     <div class="pf-c-description-list__text">${context.pk}</div> | ||||
|                 </dd> | ||||
|             </div> | ||||
|             <div class="pf-c-description-list__group"> | ||||
|                 <dt class="pf-c-description-list__term"> | ||||
|                     <span class="pf-c-description-list__text">${t`Name`}</span> | ||||
|                 </dt> | ||||
|                 <dd class="pf-c-description-list__description"> | ||||
|                     <div class="pf-c-description-list__text">${context.name}</div> | ||||
|                 </dd> | ||||
|             </div> | ||||
|             <div class="pf-c-description-list__group"> | ||||
|                 <dt class="pf-c-description-list__term"> | ||||
|                     <span class="pf-c-description-list__text">${t`App`}</span> | ||||
|                 </dt> | ||||
|                 <dd class="pf-c-description-list__description"> | ||||
|                     <div class="pf-c-description-list__text">${context.app}</div> | ||||
|                 </dd> | ||||
|             </div> | ||||
|             <div class="pf-c-description-list__group"> | ||||
|                 <dt class="pf-c-description-list__term"> | ||||
|                     <span class="pf-c-description-list__text">${t`Model Name`}</span> | ||||
|                 </dt> | ||||
|                 <dd class="pf-c-description-list__description"> | ||||
|                     <div class="pf-c-description-list__text">${context.model_name}</div> | ||||
|                 </dd> | ||||
|             </div> | ||||
|         </dl>`; | ||||
|     } | ||||
|  | ||||
|     getEmailInfo(context: EventContext): TemplateResult { | ||||
|         if (context === null) { | ||||
|             return html`<span>-</span>`; | ||||
|         } | ||||
|         return html`<dl class="pf-c-description-list pf-m-horizontal"> | ||||
|             <div class="pf-c-description-list__group"> | ||||
|                 <dt class="pf-c-description-list__term"> | ||||
|                     <span class="pf-c-description-list__text">${t`Message`}</span> | ||||
|                 </dt> | ||||
|                 <dd class="pf-c-description-list__description"> | ||||
|                     <div class="pf-c-description-list__text">${context.message}</div> | ||||
|                 </dd> | ||||
|             </div> | ||||
|             <div class="pf-c-description-list__group"> | ||||
|                 <dt class="pf-c-description-list__term"> | ||||
|                     <span class="pf-c-description-list__text">${t`Subject`}</span> | ||||
|                 </dt> | ||||
|                 <dd class="pf-c-description-list__description"> | ||||
|                     <div class="pf-c-description-list__text">${context.subject}</div> | ||||
|                 </dd> | ||||
|             </div> | ||||
|             <div class="pf-c-description-list__group"> | ||||
|                 <dt class="pf-c-description-list__term"> | ||||
|                     <span class="pf-c-description-list__text">${t`From`}</span> | ||||
|                 </dt> | ||||
|                 <dd class="pf-c-description-list__description"> | ||||
|                     <div class="pf-c-description-list__text">${context.from_email}</div> | ||||
|                 </dd> | ||||
|             </div> | ||||
|             <div class="pf-c-description-list__group"> | ||||
|                 <dt class="pf-c-description-list__term"> | ||||
|                     <span class="pf-c-description-list__text">${t`To`}</span> | ||||
|                 </dt> | ||||
|                 <dd class="pf-c-description-list__description"> | ||||
|                     <div class="pf-c-description-list__text"> | ||||
|                         ${(context.to_email as string[]).map((to) => { | ||||
|                             return html`<li>${to}</li>`; | ||||
|                         })} | ||||
|                     </div> | ||||
|                 </dd> | ||||
|             </div> | ||||
|         </dl>`; | ||||
|     } | ||||
|  | ||||
|     defaultResponse(): TemplateResult { | ||||
|         return html`<div class="pf-l-flex"> | ||||
|             <div class="pf-l-flex__item"> | ||||
|                 <h3>${t`Context`}</h3> | ||||
|                 <code>${JSON.stringify(this.event?.context, null, 4)}</code> | ||||
|             </div> | ||||
|             <div class="pf-l-flex__item"> | ||||
|                 <h3>${t`User`}</h3> | ||||
|                 <code>${JSON.stringify(this.event?.user, null, 4)}</code> | ||||
|             </div> | ||||
|         </div>`; | ||||
|     } | ||||
|  | ||||
|     buildGitHubIssueUrl(context: EventContext): string { | ||||
|         const httpRequest = this.event.context.http_request as EventContext; | ||||
|         let title = ""; | ||||
|         if (httpRequest) { | ||||
|             title = `${httpRequest?.method} ${httpRequest?.path}`; | ||||
|         } | ||||
|         // https://docs.github.com/en/issues/tracking-your-work-with-issues/creating-issues/about-automation-for-issues-and-pull-requests-with-query-parameters | ||||
|         const fullBody = ` | ||||
| **Describe the bug** | ||||
| A clear and concise description of what the bug is. | ||||
|  | ||||
| **To Reproduce** | ||||
| Steps to reproduce the behavior: | ||||
| 1. Go to '...' | ||||
| 2. Click on '....' | ||||
| 3. Scroll down to '....' | ||||
| 4. See error | ||||
|  | ||||
| **Expected behavior** | ||||
| A clear and concise description of what you expected to happen. | ||||
|  | ||||
| **Screenshots** | ||||
| If applicable, add screenshots to help explain your problem. | ||||
|  | ||||
| **Logs** | ||||
| <details> | ||||
|     <summary>Stacktrace from authentik</summary> | ||||
|  | ||||
| \`\`\` | ||||
| ${context.message as string} | ||||
| \`\`\` | ||||
| </details> | ||||
|  | ||||
|  | ||||
| **Version and Deployment (please complete the following information):** | ||||
| - authentik version: ${VERSION} | ||||
| - Deployment: [e.g. docker-compose, helm] | ||||
|  | ||||
| **Additional context** | ||||
| Add any other context about the problem here. | ||||
|         `; | ||||
|         return `https://github.com/goauthentik/authentik/issues/ | ||||
| new?labels=bug,from_authentik&title=${encodeURIComponent(title)} | ||||
| &body=${encodeURIComponent(fullBody)}`.trim(); | ||||
|     } | ||||
|  | ||||
|     render(): TemplateResult { | ||||
|         if (!this.event) { | ||||
|             return html`<ak-spinner size=${PFSize.Medium}></ak-spinner>`; | ||||
|         } | ||||
|         switch (this.event?.action) { | ||||
|             case EventActions.ModelCreated: | ||||
|             case EventActions.ModelUpdated: | ||||
|             case EventActions.ModelDeleted: | ||||
|                 return html` | ||||
|                     <h3>${t`Affected model:`}</h3> | ||||
|                     ${this.getModelInfo(this.event.context?.model as EventModel)} | ||||
|                 `; | ||||
|             case EventActions.AuthorizeApplication: | ||||
|                 return html`<div class="pf-l-flex"> | ||||
|                         <div class="pf-l-flex__item"> | ||||
|                             <h3>${t`Authorized application:`}</h3> | ||||
|                             ${this.getModelInfo( | ||||
|                                 this.event.context.authorized_application as EventModel, | ||||
|                             )} | ||||
|                         </div> | ||||
|                         <div class="pf-l-flex__item"> | ||||
|                             <h3>${t`Using flow`}</h3> | ||||
|                             <span | ||||
|                                 >${until( | ||||
|                                     new FlowsApi(DEFAULT_CONFIG) | ||||
|                                         .flowsInstancesList({ | ||||
|                                             flowUuid: this.event.context.flow as string, | ||||
|                                         }) | ||||
|                                         .then((resp) => { | ||||
|                                             return html`<a | ||||
|                                                 href="#/flow/flows/${resp.results[0].slug}" | ||||
|                                                 >${resp.results[0].name}</a | ||||
|                                             >`; | ||||
|                                         }), | ||||
|                                     html`<ak-spinner size=${PFSize.Medium}></ak-spinner>`, | ||||
|                                 )} | ||||
|                             </span> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                     <ak-expand>${this.defaultResponse()}</ak-expand>`; | ||||
|             case EventActions.EmailSent: | ||||
|                 return html`<h3>${t`Email info:`}</h3> | ||||
|                     ${this.getEmailInfo(this.event.context)} | ||||
|                     <ak-expand> | ||||
|                         <iframe srcdoc=${this.event.context.body}></iframe> | ||||
|                     </ak-expand>`; | ||||
|             case EventActions.SecretView: | ||||
|                 return html` <h3>${t`Secret:`}</h3> | ||||
|                     ${this.getModelInfo(this.event.context.secret as EventModel)}`; | ||||
|             case EventActions.SystemException: | ||||
|                 return html` <a | ||||
|                         class="pf-c-button pf-m-primary" | ||||
|                         target="_blank" | ||||
|                         href=${this.buildGitHubIssueUrl(this.event.context)} | ||||
|                     > | ||||
|                         ${t`Open issue on GitHub...`} | ||||
|                     </a> | ||||
|                     <div class="pf-l-flex"> | ||||
|                         <div class="pf-l-flex__item"> | ||||
|                             <h3>${t`Exception`}</h3> | ||||
|                             <code>${this.event.context.message}</code> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                     <ak-expand>${this.defaultResponse()}</ak-expand>`; | ||||
|             case EventActions.PropertyMappingException: | ||||
|                 return html`<div class="pf-l-flex"> | ||||
|                         <div class="pf-l-flex__item"> | ||||
|                             <h3>${t`Exception`}</h3> | ||||
|                             <code>${this.event.context.message || this.event.context.error}</code> | ||||
|                         </div> | ||||
|                         <div class="pf-l-flex__item"> | ||||
|                             <h3>${t`Expression`}</h3> | ||||
|                             <code>${this.event.context.expression}</code> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                     <ak-expand>${this.defaultResponse()}</ak-expand>`; | ||||
|             case EventActions.PolicyException: | ||||
|                 return html`<div class="pf-l-flex"> | ||||
|                         <div class="pf-l-flex__item"> | ||||
|                             <h3>${t`Binding`}</h3> | ||||
|                             ${this.getModelInfo(this.event.context.binding as EventModel)} | ||||
|                         </div> | ||||
|                         <div class="pf-l-flex__item"> | ||||
|                             <h3>${t`Request`}</h3> | ||||
|                             <ul class="pf-c-list"> | ||||
|                                 <li> | ||||
|                                     ${t`Object`}: | ||||
|                                     ${this.getModelInfo( | ||||
|                                         (this.event.context.request as EventContext) | ||||
|                                             .obj as EventModel, | ||||
|                                     )} | ||||
|                                 </li> | ||||
|                                 <li> | ||||
|                                     <span | ||||
|                                         >${t`Context`}: | ||||
|                                         <code | ||||
|                                             >${JSON.stringify( | ||||
|                                                 (this.event.context.request as EventContext) | ||||
|                                                     .context, | ||||
|                                                 null, | ||||
|                                                 4, | ||||
|                                             )}</code | ||||
|                                         ></span | ||||
|                                     > | ||||
|                                 </li> | ||||
|                             </ul> | ||||
|                         </div> | ||||
|                         <div class="pf-l-flex__item"> | ||||
|                             <h3>${t`Exception`}</h3> | ||||
|                             <code>${this.event.context.message || this.event.context.error}</code> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                     <ak-expand>${this.defaultResponse()}</ak-expand>`; | ||||
|             case EventActions.PolicyExecution: | ||||
|                 return html`<div class="pf-l-flex"> | ||||
|                         <div class="pf-l-flex__item"> | ||||
|                             <h3>${t`Binding`}</h3> | ||||
|                             ${this.getModelInfo(this.event.context.binding as EventModel)} | ||||
|                         </div> | ||||
|                         <div class="pf-l-flex__item"> | ||||
|                             <h3>${t`Request`}</h3> | ||||
|                             <ul class="pf-c-list"> | ||||
|                                 <li> | ||||
|                                     ${t`Object`}: | ||||
|                                     ${this.getModelInfo( | ||||
|                                         (this.event.context.request as EventContext) | ||||
|                                             .obj as EventModel, | ||||
|                                     )} | ||||
|                                 </li> | ||||
|                                 <li> | ||||
|                                     <span | ||||
|                                         >${t`Context`}: | ||||
|                                         <code | ||||
|                                             >${JSON.stringify( | ||||
|                                                 (this.event.context.request as EventContext) | ||||
|                                                     .context, | ||||
|                                                 null, | ||||
|                                                 4, | ||||
|                                             )}</code | ||||
|                                         ></span | ||||
|                                     > | ||||
|                                 </li> | ||||
|                             </ul> | ||||
|                         </div> | ||||
|                         <div class="pf-l-flex__item"> | ||||
|                             <h3>${t`Result`}</h3> | ||||
|                             <ul class="pf-c-list"> | ||||
|                                 <li> | ||||
|                                     ${t`Passing`}: | ||||
|                                     ${(this.event.context.result as EventContext).passing} | ||||
|                                 </li> | ||||
|                                 <li> | ||||
|                                     ${t`Messages`}: | ||||
|                                     <ul class="pf-c-list"> | ||||
|                                         ${( | ||||
|                                             (this.event.context.result as EventContext) | ||||
|                                                 .messages as string[] | ||||
|                                         ).map((msg) => { | ||||
|                                             return html`<li>${msg}</li>`; | ||||
|                                         })} | ||||
|                                     </ul> | ||||
|                                 </li> | ||||
|                             </ul> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                     <ak-expand>${this.defaultResponse()}</ak-expand>`; | ||||
|             case EventActions.ConfigurationError: | ||||
|                 return html`<h3>${this.event.context.message}</h3> | ||||
|                     <ak-expand>${this.defaultResponse()}</ak-expand>`; | ||||
|             case EventActions.UpdateAvailable: | ||||
|                 return html`<h3>${t`New version available!`}</h3> | ||||
|                     <a | ||||
|                         target="_blank" | ||||
|                         href="https://github.com/goauthentik/authentik/releases/tag/version%2F${this | ||||
|                             .event.context.new_version}" | ||||
|                     > | ||||
|                         ${this.event.context.new_version} | ||||
|                     </a>`; | ||||
|             // Action types which typically don't record any extra context. | ||||
|             // If context is not empty, we fall to the default response. | ||||
|             case EventActions.Login: | ||||
|                 if ("using_source" in this.event.context) { | ||||
|                     return html`<div class="pf-l-flex"> | ||||
|                         <div class="pf-l-flex__item"> | ||||
|                             <h3>${t`Using source`}</h3> | ||||
|                             ${this.getModelInfo(this.event.context.using_source as EventModel)} | ||||
|                         </div> | ||||
|                     </div>`; | ||||
|                 } | ||||
|                 return this.defaultResponse(); | ||||
|             case EventActions.LoginFailed: | ||||
|                 return html` <h3>${t`Attempted to log in as ${this.event.context.username}`}</h3> | ||||
|                     <ak-expand>${this.defaultResponse()}</ak-expand>`; | ||||
|             case EventActions.Logout: | ||||
|                 if (Object.keys(this.event.context).length === 0) { | ||||
|                     return html`<span>${t`No additional data available.`}</span>`; | ||||
|                 } | ||||
|                 return this.defaultResponse(); | ||||
|             default: | ||||
|                 return this.defaultResponse(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										55
									
								
								web/src/admin/events/EventInfoPage.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								web/src/admin/events/EventInfoPage.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,55 @@ | ||||
| import "@goauthentik/admin/events/EventInfo"; | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { EventWithContext } from "@goauthentik/common/events"; | ||||
| import { AKElement } from "@goauthentik/elements/Base"; | ||||
| import "@goauthentik/elements/PageHeader"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { CSSResult, TemplateResult, html } from "lit"; | ||||
| import { customElement, property } from "lit/decorators.js"; | ||||
|  | ||||
| import AKGlobal from "@goauthentik/common/styles/authentik.css"; | ||||
| import PFCard from "@patternfly/patternfly/components/Card/card.css"; | ||||
| import PFContent from "@patternfly/patternfly/components/Content/content.css"; | ||||
| import PFPage from "@patternfly/patternfly/components/Page/page.css"; | ||||
| import PFBase from "@patternfly/patternfly/patternfly-base.css"; | ||||
|  | ||||
| import { EventsApi } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-event-info-page") | ||||
| export class EventInfoPage extends AKElement { | ||||
|     @property() | ||||
|     set eventID(value: string) { | ||||
|         new EventsApi(DEFAULT_CONFIG) | ||||
|             .eventsEventsRetrieve({ | ||||
|                 eventUuid: value, | ||||
|             }) | ||||
|             .then((ev) => { | ||||
|                 this.event = ev as EventWithContext; | ||||
|             }); | ||||
|     } | ||||
|  | ||||
|     @property({ attribute: false }) | ||||
|     event!: EventWithContext; | ||||
|  | ||||
|     static get styles(): CSSResult[] { | ||||
|         return [PFBase, PFPage, PFContent, PFCard, AKGlobal]; | ||||
|     } | ||||
|  | ||||
|     render(): TemplateResult { | ||||
|         return html`<ak-page-header | ||||
|                 icon="pf-icon pf-icon-catalog" | ||||
|                 header=${t`Event ${this.event?.pk || ""}`} | ||||
|             > | ||||
|             </ak-page-header> | ||||
|             <section class="pf-c-page__main-section pf-m-no-padding-mobile"> | ||||
|                 <div class="pf-c-card"> | ||||
|                     <div class="pf-c-card__title">${t`Event info`}</div> | ||||
|                     <div class="pf-c-card__body"> | ||||
|                         <ak-event-info .event=${this.event}></ak-event-info> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </section>`; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										88
									
								
								web/src/admin/events/EventListPage.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								web/src/admin/events/EventListPage.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,88 @@ | ||||
| import "@goauthentik/admin/events/EventInfo"; | ||||
| import { ActionToLabel } from "@goauthentik/admin/events/utils"; | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { EventWithContext } from "@goauthentik/common/events"; | ||||
| import { uiConfig } from "@goauthentik/common/ui/config"; | ||||
| import { PaginatedResponse } from "@goauthentik/elements/table/Table"; | ||||
| import { TableColumn } from "@goauthentik/elements/table/Table"; | ||||
| import { TablePage } from "@goauthentik/elements/table/TablePage"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { TemplateResult, html } from "lit"; | ||||
| import { customElement, property } from "lit/decorators.js"; | ||||
|  | ||||
| import { Event, EventsApi } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-event-list") | ||||
| export class EventListPage extends TablePage<Event> { | ||||
|     expandable = true; | ||||
|  | ||||
|     pageTitle(): string { | ||||
|         return t`Event Log`; | ||||
|     } | ||||
|     pageDescription(): string | undefined { | ||||
|         return; | ||||
|     } | ||||
|     pageIcon(): string { | ||||
|         return "pf-icon pf-icon-catalog"; | ||||
|     } | ||||
|     searchEnabled(): boolean { | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     @property() | ||||
|     order = "-created"; | ||||
|  | ||||
|     async apiEndpoint(page: number): Promise<PaginatedResponse<Event>> { | ||||
|         return new EventsApi(DEFAULT_CONFIG).eventsEventsList({ | ||||
|             ordering: this.order, | ||||
|             page: page, | ||||
|             pageSize: (await uiConfig()).pagination.perPage, | ||||
|             search: this.search || "", | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     columns(): TableColumn[] { | ||||
|         return [ | ||||
|             new TableColumn(t`Action`, "action"), | ||||
|             new TableColumn(t`User`, "user"), | ||||
|             new TableColumn(t`Creation Date`, "created"), | ||||
|             new TableColumn(t`Client IP`, "client_ip"), | ||||
|             new TableColumn(t`Tenant`, "tenant_name"), | ||||
|             new TableColumn(t`Actions`), | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     row(item: EventWithContext): TemplateResult[] { | ||||
|         return [ | ||||
|             html`<div>${ActionToLabel(item.action)}</div> | ||||
|                 <small>${item.app}</small>`, | ||||
|             item.user?.username | ||||
|                 ? html`<a href="#/identity/users/${item.user.pk}"> ${item.user?.username} </a> | ||||
|                       ${item.user.on_behalf_of | ||||
|                           ? html`<small> | ||||
|                                 ${t`On behalf of ${item.user.on_behalf_of.username}`} | ||||
|                             </small>` | ||||
|                           : html``}` | ||||
|                 : html`-`, | ||||
|             html`<span>${item.created?.toLocaleString()}</span>`, | ||||
|             html`<span>${item.clientIp || t`-`}</span>`, | ||||
|             html`<span>${item.tenant?.name || t`-`}</span>`, | ||||
|             html`<a href="#/events/log/${item.pk}"> | ||||
|                 <i class="fas fa-share-square"></i> | ||||
|             </a>`, | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     renderExpanded(item: Event): TemplateResult { | ||||
|         return html` <td role="cell" colspan="3"> | ||||
|                 <div class="pf-c-table__expandable-row-content"> | ||||
|                     <ak-event-info .event=${item as EventWithContext}></ak-event-info> | ||||
|                 </div> | ||||
|             </td> | ||||
|             <td></td> | ||||
|             <td></td> | ||||
|             <td></td>`; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										133
									
								
								web/src/admin/events/RuleForm.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										133
									
								
								web/src/admin/events/RuleForm.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,133 @@ | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import "@goauthentik/elements/forms/HorizontalFormElement"; | ||||
| import { ModelForm } from "@goauthentik/elements/forms/ModelForm"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { TemplateResult, html } from "lit"; | ||||
| import { customElement } from "lit/decorators.js"; | ||||
| import { ifDefined } from "lit/directives/if-defined.js"; | ||||
| import { until } from "lit/directives/until.js"; | ||||
|  | ||||
| import { CoreApi, EventsApi, NotificationRule, SeverityEnum } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-event-rule-form") | ||||
| export class RuleForm extends ModelForm<NotificationRule, string> { | ||||
|     loadInstance(pk: string): Promise<NotificationRule> { | ||||
|         return new EventsApi(DEFAULT_CONFIG).eventsRulesRetrieve({ | ||||
|             pbmUuid: pk, | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     getSuccessMessage(): string { | ||||
|         if (this.instance) { | ||||
|             return t`Successfully updated rule.`; | ||||
|         } else { | ||||
|             return t`Successfully created rule.`; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     send = (data: NotificationRule): Promise<NotificationRule> => { | ||||
|         if (this.instance) { | ||||
|             return new EventsApi(DEFAULT_CONFIG).eventsRulesUpdate({ | ||||
|                 pbmUuid: this.instance.pk || "", | ||||
|                 notificationRuleRequest: data, | ||||
|             }); | ||||
|         } else { | ||||
|             return new EventsApi(DEFAULT_CONFIG).eventsRulesCreate({ | ||||
|                 notificationRuleRequest: data, | ||||
|             }); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     renderSeverity(): TemplateResult { | ||||
|         return html` | ||||
|             <option | ||||
|                 value=${SeverityEnum.Alert} | ||||
|                 ?selected=${this.instance?.severity === SeverityEnum.Alert} | ||||
|             > | ||||
|                 ${t`Alert`} | ||||
|             </option> | ||||
|             <option | ||||
|                 value=${SeverityEnum.Warning} | ||||
|                 ?selected=${this.instance?.severity === SeverityEnum.Warning} | ||||
|             > | ||||
|                 ${t`Warning`} | ||||
|             </option> | ||||
|             <option | ||||
|                 value=${SeverityEnum.Notice} | ||||
|                 ?selected=${this.instance?.severity === SeverityEnum.Notice} | ||||
|             > | ||||
|                 ${t`Notice`} | ||||
|             </option> | ||||
|         `; | ||||
|     } | ||||
|  | ||||
|     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="${ifDefined(this.instance?.name)}" | ||||
|                     class="pf-c-form-control" | ||||
|                     required | ||||
|                 /> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-element-horizontal label=${t`Group`} name="group"> | ||||
|                 <select class="pf-c-form-control"> | ||||
|                     <option value="" ?selected=${this.instance?.group === undefined}> | ||||
|                         --------- | ||||
|                     </option> | ||||
|                     ${until( | ||||
|                         new CoreApi(DEFAULT_CONFIG).coreGroupsList({}).then((groups) => { | ||||
|                             return groups.results.map((group) => { | ||||
|                                 return html`<option | ||||
|                                     value=${ifDefined(group.pk)} | ||||
|                                     ?selected=${this.instance?.group === group.pk} | ||||
|                                 > | ||||
|                                     ${group.name} | ||||
|                                 </option>`; | ||||
|                             }); | ||||
|                         }), | ||||
|                         html`<option>${t`Loading...`}</option>`, | ||||
|                     )} | ||||
|                 </select> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-element-horizontal label=${t`Transports`} ?required=${true} name="transports"> | ||||
|                 <select name="users" class="pf-c-form-control" multiple> | ||||
|                     ${until( | ||||
|                         new EventsApi(DEFAULT_CONFIG) | ||||
|                             .eventsTransportsList({}) | ||||
|                             .then((transports) => { | ||||
|                                 return transports.results.map((transport) => { | ||||
|                                     const selected = Array.from( | ||||
|                                         this.instance?.transports || [], | ||||
|                                     ).some((su) => { | ||||
|                                         return su == transport.pk; | ||||
|                                     }); | ||||
|                                     return html`<option | ||||
|                                         value=${ifDefined(transport.pk)} | ||||
|                                         ?selected=${selected} | ||||
|                                     > | ||||
|                                         ${transport.name} | ||||
|                                     </option>`; | ||||
|                                 }); | ||||
|                             }), | ||||
|                         html`<option>${t`Loading...`}</option>`, | ||||
|                     )} | ||||
|                 </select> | ||||
|                 <p class="pf-c-form__helper-text"> | ||||
|                     ${t`Select which transports should be used to notify the user. If none are selected, the notification will only be shown in the authentik UI.`} | ||||
|                 </p> | ||||
|                 <p class="pf-c-form__helper-text"> | ||||
|                     ${t`Hold control/command to select multiple items.`} | ||||
|                 </p> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-element-horizontal label=${t`Severity`} ?required=${true} name="severity"> | ||||
|                 <select class="pf-c-form-control"> | ||||
|                     ${this.renderSeverity()} | ||||
|                 </select> | ||||
|             </ak-form-element-horizontal> | ||||
|         </form>`; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										118
									
								
								web/src/admin/events/RuleListPage.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										118
									
								
								web/src/admin/events/RuleListPage.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,118 @@ | ||||
| import "@goauthentik/admin/events/RuleForm"; | ||||
| import "@goauthentik/admin/policies/BoundPoliciesList"; | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { uiConfig } from "@goauthentik/common/ui/config"; | ||||
| import "@goauthentik/elements/buttons/SpinnerButton"; | ||||
| import "@goauthentik/elements/forms/DeleteBulkForm"; | ||||
| import "@goauthentik/elements/forms/ModalForm"; | ||||
| import { PaginatedResponse } from "@goauthentik/elements/table/Table"; | ||||
| import { TableColumn } from "@goauthentik/elements/table/Table"; | ||||
| import { TablePage } from "@goauthentik/elements/table/TablePage"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { TemplateResult, html } from "lit"; | ||||
| import { customElement, property } from "lit/decorators.js"; | ||||
|  | ||||
| import { EventsApi, NotificationRule } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-event-rule-list") | ||||
| export class RuleListPage extends TablePage<NotificationRule> { | ||||
|     expandable = true; | ||||
|     checkbox = true; | ||||
|  | ||||
|     searchEnabled(): boolean { | ||||
|         return true; | ||||
|     } | ||||
|     pageTitle(): string { | ||||
|         return t`Notification Rules`; | ||||
|     } | ||||
|     pageDescription(): string { | ||||
|         return t`Send notifications whenever a specific Event is created and matched by policies.`; | ||||
|     } | ||||
|     pageIcon(): string { | ||||
|         return "pf-icon pf-icon-attention-bell"; | ||||
|     } | ||||
|  | ||||
|     @property() | ||||
|     order = "name"; | ||||
|  | ||||
|     async apiEndpoint(page: number): Promise<PaginatedResponse<NotificationRule>> { | ||||
|         return new EventsApi(DEFAULT_CONFIG).eventsRulesList({ | ||||
|             ordering: this.order, | ||||
|             page: page, | ||||
|             pageSize: (await uiConfig()).pagination.perPage, | ||||
|             search: this.search || "", | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     columns(): TableColumn[] { | ||||
|         return [ | ||||
|             new TableColumn(t`Name`, "name"), | ||||
|             new TableColumn(t`Severity`, "severity"), | ||||
|             new TableColumn(t`Sent to group`, "group"), | ||||
|             new TableColumn(t`Actions`), | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     renderToolbarSelected(): TemplateResult { | ||||
|         const disabled = this.selectedElements.length < 1; | ||||
|         return html`<ak-forms-delete-bulk | ||||
|             objectLabel=${t`Notification rule(s)`} | ||||
|             .objects=${this.selectedElements} | ||||
|             .usedBy=${(item: NotificationRule) => { | ||||
|                 return new EventsApi(DEFAULT_CONFIG).eventsRulesUsedByList({ | ||||
|                     pbmUuid: item.pk, | ||||
|                 }); | ||||
|             }} | ||||
|             .delete=${(item: NotificationRule) => { | ||||
|                 return new EventsApi(DEFAULT_CONFIG).eventsRulesDestroy({ | ||||
|                     pbmUuid: item.pk, | ||||
|                 }); | ||||
|             }} | ||||
|         > | ||||
|             <button ?disabled=${disabled} slot="trigger" class="pf-c-button pf-m-danger"> | ||||
|                 ${t`Delete`} | ||||
|             </button> | ||||
|         </ak-forms-delete-bulk>`; | ||||
|     } | ||||
|  | ||||
|     row(item: NotificationRule): TemplateResult[] { | ||||
|         return [ | ||||
|             html`${item.name}`, | ||||
|             html`${item.severity}`, | ||||
|             html`${item.groupObj?.name || t`None (rule disabled)`}`, | ||||
|             html`<ak-forms-modal> | ||||
|                 <span slot="submit"> ${t`Update`} </span> | ||||
|                 <span slot="header"> ${t`Update Notification Rule`} </span> | ||||
|                 <ak-event-rule-form slot="form" .instancePk=${item.pk}> </ak-event-rule-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 Notification Rule`} </span> | ||||
|                 <ak-event-rule-form slot="form"> </ak-event-rule-form> | ||||
|                 <button slot="trigger" class="pf-c-button pf-m-primary">${t`Create`}</button> | ||||
|             </ak-forms-modal> | ||||
|         `; | ||||
|     } | ||||
|  | ||||
|     renderExpanded(item: NotificationRule): TemplateResult { | ||||
|         return html` <td role="cell" colspan="4"> | ||||
|             <div class="pf-c-table__expandable-row-content"> | ||||
|                 <p> | ||||
|                     ${t`These bindings control upon which events this rule triggers. Bindings to | ||||
|                 groups/users are checked against the user of the event.`} | ||||
|                 </p> | ||||
|                 <ak-bound-policies-list .target=${item.pk}> </ak-bound-policies-list> | ||||
|             </div> | ||||
|         </td>`; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										171
									
								
								web/src/admin/events/TransportForm.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										171
									
								
								web/src/admin/events/TransportForm.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,171 @@ | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { first } from "@goauthentik/common/utils"; | ||||
| import "@goauthentik/elements/forms/HorizontalFormElement"; | ||||
| import { ModelForm } from "@goauthentik/elements/forms/ModelForm"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { TemplateResult, html } from "lit"; | ||||
| import { customElement, property } from "lit/decorators.js"; | ||||
| import { ifDefined } from "lit/directives/if-defined.js"; | ||||
| import { until } from "lit/directives/until.js"; | ||||
|  | ||||
| import { | ||||
|     EventsApi, | ||||
|     NotificationTransport, | ||||
|     NotificationTransportModeEnum, | ||||
|     PropertymappingsApi, | ||||
| } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-event-transport-form") | ||||
| export class TransportForm extends ModelForm<NotificationTransport, string> { | ||||
|     loadInstance(pk: string): Promise<NotificationTransport> { | ||||
|         return new EventsApi(DEFAULT_CONFIG) | ||||
|             .eventsTransportsRetrieve({ | ||||
|                 uuid: pk, | ||||
|             }) | ||||
|             .then((transport) => { | ||||
|                 this.onModeChange(transport.mode); | ||||
|                 return transport; | ||||
|             }); | ||||
|     } | ||||
|  | ||||
|     @property({ type: Boolean }) | ||||
|     showWebhook = false; | ||||
|  | ||||
|     getSuccessMessage(): string { | ||||
|         if (this.instance) { | ||||
|             return t`Successfully updated transport.`; | ||||
|         } else { | ||||
|             return t`Successfully created transport.`; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     send = (data: NotificationTransport): Promise<NotificationTransport> => { | ||||
|         if (this.instance) { | ||||
|             return new EventsApi(DEFAULT_CONFIG).eventsTransportsUpdate({ | ||||
|                 uuid: this.instance.pk || "", | ||||
|                 notificationTransportRequest: data, | ||||
|             }); | ||||
|         } else { | ||||
|             return new EventsApi(DEFAULT_CONFIG).eventsTransportsCreate({ | ||||
|                 notificationTransportRequest: data, | ||||
|             }); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     renderTransportModes(): TemplateResult { | ||||
|         return html` | ||||
|             <option | ||||
|                 value=${NotificationTransportModeEnum.Local} | ||||
|                 ?selected=${this.instance?.mode === NotificationTransportModeEnum.Local} | ||||
|             > | ||||
|                 ${t`Local (notifications will be created within authentik)`} | ||||
|             </option> | ||||
|             <option | ||||
|                 value=${NotificationTransportModeEnum.Email} | ||||
|                 ?selected=${this.instance?.mode === NotificationTransportModeEnum.Email} | ||||
|             > | ||||
|                 ${t`Email`} | ||||
|             </option> | ||||
|             <option | ||||
|                 value=${NotificationTransportModeEnum.Webhook} | ||||
|                 ?selected=${this.instance?.mode === NotificationTransportModeEnum.Webhook} | ||||
|             > | ||||
|                 ${t`Webhook (generic)`} | ||||
|             </option> | ||||
|             <option | ||||
|                 value=${NotificationTransportModeEnum.WebhookSlack} | ||||
|                 ?selected=${this.instance?.mode === NotificationTransportModeEnum.WebhookSlack} | ||||
|             > | ||||
|                 ${t`Webhook (Slack/Discord)`} | ||||
|             </option> | ||||
|         `; | ||||
|     } | ||||
|  | ||||
|     onModeChange(mode: string | undefined): void { | ||||
|         if ( | ||||
|             mode === NotificationTransportModeEnum.Webhook || | ||||
|             mode === NotificationTransportModeEnum.WebhookSlack | ||||
|         ) { | ||||
|             this.showWebhook = true; | ||||
|         } else { | ||||
|             this.showWebhook = false; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     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="${ifDefined(this.instance?.name)}" | ||||
|                     class="pf-c-form-control" | ||||
|                     required | ||||
|                 /> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-element-horizontal label=${t`Mode`} ?required=${true} name="mode"> | ||||
|                 <select | ||||
|                     class="pf-c-form-control" | ||||
|                     @change=${(ev: Event) => { | ||||
|                         const current = (ev.target as HTMLInputElement).value; | ||||
|                         this.onModeChange(current); | ||||
|                     }} | ||||
|                 > | ||||
|                     ${this.renderTransportModes()} | ||||
|                 </select> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-element-horizontal | ||||
|                 ?hidden=${!this.showWebhook} | ||||
|                 label=${t`Webhook URL`} | ||||
|                 name="webhookUrl" | ||||
|                 ?required=${true} | ||||
|             > | ||||
|                 <input | ||||
|                     type="text" | ||||
|                     value="${ifDefined(this.instance?.webhookUrl)}" | ||||
|                     class="pf-c-form-control" | ||||
|                 /> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-element-horizontal | ||||
|                 ?hidden=${!this.showWebhook} | ||||
|                 label=${t`Webhook Mapping`} | ||||
|                 name="webhookMapping" | ||||
|             > | ||||
|                 <select class="pf-c-form-control"> | ||||
|                     <option value="" ?selected=${this.instance?.webhookMapping === undefined}> | ||||
|                         --------- | ||||
|                     </option> | ||||
|                     ${until( | ||||
|                         new PropertymappingsApi(DEFAULT_CONFIG) | ||||
|                             .propertymappingsNotificationList({}) | ||||
|                             .then((mappings) => { | ||||
|                                 return mappings.results.map((mapping) => { | ||||
|                                     return html`<option | ||||
|                                         value=${ifDefined(mapping.pk)} | ||||
|                                         ?selected=${this.instance?.webhookMapping === mapping.pk} | ||||
|                                     > | ||||
|                                         ${mapping.name} | ||||
|                                     </option>`; | ||||
|                                 }); | ||||
|                             }), | ||||
|                         html`<option>${t`Loading...`}</option>`, | ||||
|                     )} | ||||
|                 </select> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-element-horizontal name="sendOnce"> | ||||
|                 <div class="pf-c-check"> | ||||
|                     <input | ||||
|                         type="checkbox" | ||||
|                         class="pf-c-check__input" | ||||
|                         ?checked=${first(this.instance?.sendOnce, false)} | ||||
|                     /> | ||||
|                     <label class="pf-c-check__label"> ${t`Send once`} </label> | ||||
|                 </div> | ||||
|                 <p class="pf-c-form__helper-text"> | ||||
|                     ${t`Only send notification once, for example when sending a webhook into a chat channel.`} | ||||
|                 </p> | ||||
|             </ak-form-element-horizontal> | ||||
|         </form>`; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										114
									
								
								web/src/admin/events/TransportListPage.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										114
									
								
								web/src/admin/events/TransportListPage.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,114 @@ | ||||
| import "@goauthentik/admin/events/TransportForm"; | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { uiConfig } from "@goauthentik/common/ui/config"; | ||||
| import "@goauthentik/elements/buttons/ActionButton"; | ||||
| import "@goauthentik/elements/buttons/SpinnerButton"; | ||||
| import "@goauthentik/elements/forms/DeleteBulkForm"; | ||||
| import "@goauthentik/elements/forms/ModalForm"; | ||||
| import { PaginatedResponse } from "@goauthentik/elements/table/Table"; | ||||
| import { TableColumn } from "@goauthentik/elements/table/Table"; | ||||
| import { TablePage } from "@goauthentik/elements/table/TablePage"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { TemplateResult, html } from "lit"; | ||||
| import { customElement, property } from "lit/decorators.js"; | ||||
|  | ||||
| import { EventsApi, NotificationTransport } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-event-transport-list") | ||||
| export class TransportListPage extends TablePage<NotificationTransport> { | ||||
|     searchEnabled(): boolean { | ||||
|         return true; | ||||
|     } | ||||
|     pageTitle(): string { | ||||
|         return t`Notification Transports`; | ||||
|     } | ||||
|     pageDescription(): string { | ||||
|         return t`Define how notifications are sent to users, like Email or Webhook.`; | ||||
|     } | ||||
|     pageIcon(): string { | ||||
|         return "pf-icon pf-icon-export"; | ||||
|     } | ||||
|  | ||||
|     checkbox = true; | ||||
|  | ||||
|     @property() | ||||
|     order = "name"; | ||||
|  | ||||
|     async apiEndpoint(page: number): Promise<PaginatedResponse<NotificationTransport>> { | ||||
|         return new EventsApi(DEFAULT_CONFIG).eventsTransportsList({ | ||||
|             ordering: this.order, | ||||
|             page: page, | ||||
|             pageSize: (await uiConfig()).pagination.perPage, | ||||
|             search: this.search || "", | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     columns(): TableColumn[] { | ||||
|         return [ | ||||
|             new TableColumn(t`Name`, "name"), | ||||
|             new TableColumn(t`Mode`, "mode"), | ||||
|             new TableColumn(t`Actions`), | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     renderToolbarSelected(): TemplateResult { | ||||
|         const disabled = this.selectedElements.length < 1; | ||||
|         return html`<ak-forms-delete-bulk | ||||
|             objectLabel=${t`Notification transport(s)`} | ||||
|             .objects=${this.selectedElements} | ||||
|             .usedBy=${(item: NotificationTransport) => { | ||||
|                 return new EventsApi(DEFAULT_CONFIG).eventsTransportsUsedByList({ | ||||
|                     uuid: item.pk, | ||||
|                 }); | ||||
|             }} | ||||
|             .delete=${(item: NotificationTransport) => { | ||||
|                 return new EventsApi(DEFAULT_CONFIG).eventsTransportsDestroy({ | ||||
|                     uuid: item.pk, | ||||
|                 }); | ||||
|             }} | ||||
|         > | ||||
|             <button ?disabled=${disabled} slot="trigger" class="pf-c-button pf-m-danger"> | ||||
|                 ${t`Delete`} | ||||
|             </button> | ||||
|         </ak-forms-delete-bulk>`; | ||||
|     } | ||||
|  | ||||
|     row(item: NotificationTransport): TemplateResult[] { | ||||
|         return [ | ||||
|             html`${item.name}`, | ||||
|             html`${item.modeVerbose}`, | ||||
|             html`<ak-forms-modal> | ||||
|                     <span slot="submit"> ${t`Update`} </span> | ||||
|                     <span slot="header"> ${t`Update Notification Transport`} </span> | ||||
|                     <ak-event-transport-form slot="form" .instancePk=${item.pk}> | ||||
|                     </ak-event-transport-form> | ||||
|                     <button slot="trigger" class="pf-c-button pf-m-plain"> | ||||
|                         <i class="fas fa-edit"></i> | ||||
|                     </button> | ||||
|                 </ak-forms-modal> | ||||
|                 <ak-action-button | ||||
|                     class="pf-m-plain" | ||||
|                     .apiRequest=${() => { | ||||
|                         return new EventsApi(DEFAULT_CONFIG).eventsTransportsTestCreate({ | ||||
|                             uuid: item.pk || "", | ||||
|                         }); | ||||
|                     }} | ||||
|                 > | ||||
|                     <i class="fas fa-vial" aria-hidden="true"></i> | ||||
|                 </ak-action-button>`, | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     renderObjectCreate(): TemplateResult { | ||||
|         return html` | ||||
|             <ak-forms-modal> | ||||
|                 <span slot="submit"> ${t`Create`} </span> | ||||
|                 <span slot="header"> ${t`Create Notification Transport`} </span> | ||||
|                 <ak-event-transport-form slot="form"> </ak-event-transport-form> | ||||
|                 <button slot="trigger" class="pf-c-button pf-m-primary">${t`Create`}</button> | ||||
|             </ak-forms-modal> | ||||
|         `; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										63
									
								
								web/src/admin/events/utils.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								web/src/admin/events/utils.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,63 @@ | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { EventActions } from "@goauthentik/api"; | ||||
|  | ||||
| export function ActionToLabel(action?: EventActions): string { | ||||
|     if (!action) return ""; | ||||
|     switch (action) { | ||||
|         case EventActions.Login: | ||||
|             return t`Login`; | ||||
|         case EventActions.LoginFailed: | ||||
|             return t`Failed login`; | ||||
|         case EventActions.Logout: | ||||
|             return t`Logout`; | ||||
|         case EventActions.UserWrite: | ||||
|             return t`User was written to`; | ||||
|         case EventActions.SuspiciousRequest: | ||||
|             return t`Suspicious request`; | ||||
|         case EventActions.PasswordSet: | ||||
|             return t`Password set`; | ||||
|         case EventActions.SecretView: | ||||
|             return t`Secret was viewed`; | ||||
|         case EventActions.SecretRotate: | ||||
|             return t`Secret was rotated`; | ||||
|         case EventActions.InvitationUsed: | ||||
|             return t`Invitation used`; | ||||
|         case EventActions.AuthorizeApplication: | ||||
|             return t`Application authorized`; | ||||
|         case EventActions.SourceLinked: | ||||
|             return t`Source linked`; | ||||
|         case EventActions.ImpersonationStarted: | ||||
|             return t`Impersonation started`; | ||||
|         case EventActions.ImpersonationEnded: | ||||
|             return t`Impersonation ended`; | ||||
|         case EventActions.FlowExecution: | ||||
|             return t`Flow execution`; | ||||
|         case EventActions.PolicyExecution: | ||||
|             return t`Policy execution`; | ||||
|         case EventActions.PolicyException: | ||||
|             return t`Policy exception`; | ||||
|         case EventActions.PropertyMappingException: | ||||
|             return t`Property Mapping exception`; | ||||
|         case EventActions.SystemTaskExecution: | ||||
|             return t`System task execution`; | ||||
|         case EventActions.SystemTaskException: | ||||
|             return t`System task exception`; | ||||
|         case EventActions.SystemException: | ||||
|             return t`General system exception`; | ||||
|         case EventActions.ConfigurationError: | ||||
|             return t`Configuration error`; | ||||
|         case EventActions.ModelCreated: | ||||
|             return t`Model created`; | ||||
|         case EventActions.ModelUpdated: | ||||
|             return t`Model updated`; | ||||
|         case EventActions.ModelDeleted: | ||||
|             return t`Model deleted`; | ||||
|         case EventActions.EmailSent: | ||||
|             return t`Email sent`; | ||||
|         case EventActions.UpdateAvailable: | ||||
|             return t`Update available`; | ||||
|         default: | ||||
|             return action; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										155
									
								
								web/src/admin/flows/BoundStagesList.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										155
									
								
								web/src/admin/flows/BoundStagesList.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,155 @@ | ||||
| import "@goauthentik/admin/flows/StageBindingForm"; | ||||
| import "@goauthentik/admin/policies/BoundPoliciesList"; | ||||
| import "@goauthentik/admin/stages/StageWizard"; | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { uiConfig } from "@goauthentik/common/ui/config"; | ||||
| import "@goauthentik/elements/Tabs"; | ||||
| import "@goauthentik/elements/forms/DeleteBulkForm"; | ||||
| import "@goauthentik/elements/forms/ModalForm"; | ||||
| import "@goauthentik/elements/forms/ProxyForm"; | ||||
| import { PaginatedResponse } from "@goauthentik/elements/table/Table"; | ||||
| import { Table, TableColumn } from "@goauthentik/elements/table/Table"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { TemplateResult, html } from "lit"; | ||||
| import { customElement, property } from "lit/decorators.js"; | ||||
| import { ifDefined } from "lit/directives/if-defined.js"; | ||||
|  | ||||
| import { FlowStageBinding, FlowsApi } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-bound-stages-list") | ||||
| export class BoundStagesList extends Table<FlowStageBinding> { | ||||
|     expandable = true; | ||||
|     checkbox = true; | ||||
|  | ||||
|     @property() | ||||
|     target?: string; | ||||
|  | ||||
|     async apiEndpoint(page: number): Promise<PaginatedResponse<FlowStageBinding>> { | ||||
|         return new FlowsApi(DEFAULT_CONFIG).flowsBindingsList({ | ||||
|             target: this.target || "", | ||||
|             ordering: "order", | ||||
|             page: page, | ||||
|             pageSize: (await uiConfig()).pagination.perPage, | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     columns(): TableColumn[] { | ||||
|         return [ | ||||
|             new TableColumn(t`Order`), | ||||
|             new TableColumn(t`Name`), | ||||
|             new TableColumn(t`Type`), | ||||
|             new TableColumn(t`Actions`), | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     renderToolbarSelected(): TemplateResult { | ||||
|         const disabled = this.selectedElements.length < 1; | ||||
|         return html`<ak-forms-delete-bulk | ||||
|             objectLabel=${t`Stage binding(s)`} | ||||
|             .objects=${this.selectedElements} | ||||
|             .metadata=${(item: FlowStageBinding) => { | ||||
|                 return [ | ||||
|                     { key: t`Stage`, value: item.stageObj?.name || "" }, | ||||
|                     { key: t`Stage type`, value: item.stageObj?.verboseName || "" }, | ||||
|                 ]; | ||||
|             }} | ||||
|             .usedBy=${(item: FlowStageBinding) => { | ||||
|                 return new FlowsApi(DEFAULT_CONFIG).flowsBindingsUsedByList({ | ||||
|                     fsbUuid: item.pk, | ||||
|                 }); | ||||
|             }} | ||||
|             .delete=${(item: FlowStageBinding) => { | ||||
|                 return new FlowsApi(DEFAULT_CONFIG).flowsBindingsDestroy({ | ||||
|                     fsbUuid: item.pk, | ||||
|                 }); | ||||
|             }} | ||||
|         > | ||||
|             <button ?disabled=${disabled} slot="trigger" class="pf-c-button pf-m-danger"> | ||||
|                 ${t`Delete`} | ||||
|             </button> | ||||
|         </ak-forms-delete-bulk>`; | ||||
|     } | ||||
|  | ||||
|     row(item: FlowStageBinding): TemplateResult[] { | ||||
|         return [ | ||||
|             html`${item.order}`, | ||||
|             html`${item.stageObj?.name}`, | ||||
|             html`${item.stageObj?.verboseName}`, | ||||
|             html` <ak-forms-modal> | ||||
|                     <span slot="submit"> ${t`Update`} </span> | ||||
|                     <span slot="header"> ${t`Update ${item.stageObj?.verboseName}`} </span> | ||||
|                     <ak-proxy-form | ||||
|                         slot="form" | ||||
|                         .args=${{ | ||||
|                             instancePk: item.stage, | ||||
|                         }} | ||||
|                         type=${ifDefined(item.stageObj?.component)} | ||||
|                     > | ||||
|                     </ak-proxy-form> | ||||
|                     <button slot="trigger" class="pf-c-button pf-m-secondary"> | ||||
|                         ${t`Edit Stage`} | ||||
|                     </button> | ||||
|                 </ak-forms-modal> | ||||
|                 <ak-forms-modal> | ||||
|                     <span slot="submit"> ${t`Update`} </span> | ||||
|                     <span slot="header"> ${t`Update Stage binding`} </span> | ||||
|                     <ak-stage-binding-form slot="form" .instancePk=${item.pk}> | ||||
|                     </ak-stage-binding-form> | ||||
|                     <button slot="trigger" class="pf-c-button pf-m-secondary"> | ||||
|                         ${t`Edit Binding`} | ||||
|                     </button> | ||||
|                 </ak-forms-modal>`, | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     renderExpanded(item: FlowStageBinding): TemplateResult { | ||||
|         return html` <td></td> | ||||
|             <td role="cell" colspan="4"> | ||||
|                 <div class="pf-c-table__expandable-row-content"> | ||||
|                     <div class="pf-c-content"> | ||||
|                         <p> | ||||
|                             ${t`These bindings control if this stage will be applied to the flow.`} | ||||
|                         </p> | ||||
|                         <ak-bound-policies-list .target=${item.policybindingmodelPtrId}> | ||||
|                         </ak-bound-policies-list> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </td>`; | ||||
|     } | ||||
|  | ||||
|     renderEmpty(): TemplateResult { | ||||
|         return super.renderEmpty(html`<ak-empty-state | ||||
|             header=${t`No Stages bound`} | ||||
|             icon="pf-icon-module" | ||||
|         > | ||||
|             <div slot="body">${t`No stages are currently bound to this flow.`}</div> | ||||
|             <div slot="primary"> | ||||
|                 <ak-forms-modal> | ||||
|                     <span slot="submit"> ${t`Create`} </span> | ||||
|                     <span slot="header"> ${t`Create Stage binding`} </span> | ||||
|                     <ak-stage-binding-form slot="form" targetPk=${ifDefined(this.target)}> | ||||
|                     </ak-stage-binding-form> | ||||
|                     <button slot="trigger" class="pf-c-button pf-m-primary"> | ||||
|                         ${t`Bind stage`} | ||||
|                     </button> | ||||
|                 </ak-forms-modal> | ||||
|             </div> | ||||
|         </ak-empty-state>`); | ||||
|     } | ||||
|  | ||||
|     renderToolbar(): TemplateResult { | ||||
|         return html` | ||||
|             <ak-forms-modal> | ||||
|                 <span slot="submit"> ${t`Create`} </span> | ||||
|                 <span slot="header"> ${t`Create Stage binding`} </span> | ||||
|                 <ak-stage-binding-form slot="form" targetPk=${ifDefined(this.target)}> | ||||
|                 </ak-stage-binding-form> | ||||
|                 <button slot="trigger" class="pf-c-button pf-m-primary">${t`Bind stage`}</button> | ||||
|             </ak-forms-modal> | ||||
|             <ak-stage-wizard createText=${t`Create Stage`}></ak-stage-wizard> | ||||
|             ${super.renderToolbar()} | ||||
|         `; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										110
									
								
								web/src/admin/flows/FlowDiagram.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										110
									
								
								web/src/admin/flows/FlowDiagram.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,110 @@ | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { EVENT_REFRESH } from "@goauthentik/common/constants"; | ||||
| import { AKElement } from "@goauthentik/elements/Base"; | ||||
| import "@goauthentik/elements/EmptyState"; | ||||
| import FlowChart from "flowchart.js"; | ||||
|  | ||||
| import { TemplateResult, html } from "lit"; | ||||
| import { customElement, property } from "lit/decorators.js"; | ||||
|  | ||||
| import { FlowsApi } from "@goauthentik/api"; | ||||
|  | ||||
| export const FONT_COLOUR_DARK_MODE = "#fafafa"; | ||||
| export const FONT_COLOUR_LIGHT_MODE = "#151515"; | ||||
| export const FILL_DARK_MODE = "#18191a"; | ||||
| export const FILL_LIGHT_MODE = "#f0f0f0"; | ||||
|  | ||||
| @customElement("ak-flow-diagram") | ||||
| export class FlowDiagram extends AKElement { | ||||
|     _flowSlug?: string; | ||||
|  | ||||
|     @property() | ||||
|     set flowSlug(value: string) { | ||||
|         this._flowSlug = value; | ||||
|         this.diagram = undefined; | ||||
|         new FlowsApi(DEFAULT_CONFIG) | ||||
|             .flowsInstancesDiagramRetrieve({ | ||||
|                 slug: value, | ||||
|             }) | ||||
|             .then((data) => { | ||||
|                 this.diagram = data.diagram; | ||||
|                 this.requestUpdate(); | ||||
|             }); | ||||
|     } | ||||
|  | ||||
|     @property({ attribute: false }) | ||||
|     diagram?: string; | ||||
|  | ||||
|     @property() | ||||
|     fontColour: string = FONT_COLOUR_DARK_MODE; | ||||
|  | ||||
|     @property() | ||||
|     fill: string = FILL_DARK_MODE; | ||||
|  | ||||
|     handlerBound = false; | ||||
|  | ||||
|     createRenderRoot(): Element | ShadowRoot { | ||||
|         return this; | ||||
|     } | ||||
|  | ||||
|     get isInViewport(): boolean { | ||||
|         const rect = this.getBoundingClientRect(); | ||||
|         return !(rect.x + rect.y + rect.width + rect.height === 0); | ||||
|     } | ||||
|  | ||||
|     constructor() { | ||||
|         super(); | ||||
|         const matcher = window.matchMedia("(prefers-color-scheme: light)"); | ||||
|         const handler = (ev?: MediaQueryListEvent) => { | ||||
|             if (ev?.matches || matcher.matches) { | ||||
|                 this.fontColour = FONT_COLOUR_LIGHT_MODE; | ||||
|                 this.fill = FILL_LIGHT_MODE; | ||||
|             } else { | ||||
|                 this.fontColour = FONT_COLOUR_DARK_MODE; | ||||
|                 this.fill = FILL_DARK_MODE; | ||||
|             } | ||||
|             this.requestUpdate(); | ||||
|         }; | ||||
|         matcher.addEventListener("change", handler); | ||||
|         handler(); | ||||
|     } | ||||
|  | ||||
|     firstUpdated(): void { | ||||
|         if (this.handlerBound) return; | ||||
|         window.addEventListener(EVENT_REFRESH, this.refreshHandler); | ||||
|         this.handlerBound = true; | ||||
|     } | ||||
|  | ||||
|     refreshHandler = (): void => { | ||||
|         if (!this._flowSlug) return; | ||||
|         this.flowSlug = this._flowSlug; | ||||
|     }; | ||||
|  | ||||
|     disconnectedCallback(): void { | ||||
|         super.disconnectedCallback(); | ||||
|         window.removeEventListener(EVENT_REFRESH, this.refreshHandler); | ||||
|     } | ||||
|  | ||||
|     render(): TemplateResult { | ||||
|         this.querySelectorAll("*").forEach((el) => { | ||||
|             try { | ||||
|                 el.remove(); | ||||
|             } catch { | ||||
|                 console.debug(`authentik/flow/diagram: failed to remove element ${el}`); | ||||
|             } | ||||
|         }); | ||||
|         if (this.diagram) { | ||||
|             const diagram = FlowChart.parse(this.diagram); | ||||
|             diagram.drawSVG(this, { | ||||
|                 "font-color": this.fontColour, | ||||
|                 "line-color": "#bebebe", | ||||
|                 "element-color": "#bebebe", | ||||
|                 "fill": this.fill, | ||||
|                 "yes-text": "Policy passes", | ||||
|                 "no-text": "Policy denies", | ||||
|             }); | ||||
|             return html``; | ||||
|         } | ||||
|         return html`<ak-empty-state ?loading=${true}></ak-empty-state>`; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										331
									
								
								web/src/admin/flows/FlowForm.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										331
									
								
								web/src/admin/flows/FlowForm.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,331 @@ | ||||
| import { DesignationToLabel, LayoutToLabel } from "@goauthentik/admin/flows/utils"; | ||||
| import { DEFAULT_CONFIG, config } from "@goauthentik/common/api/config"; | ||||
| import { first } from "@goauthentik/common/utils"; | ||||
| import "@goauthentik/elements/forms/HorizontalFormElement"; | ||||
| import { ModelForm } from "@goauthentik/elements/forms/ModelForm"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { TemplateResult, html } from "lit"; | ||||
| import { customElement, property } from "lit/decorators.js"; | ||||
| import { ifDefined } from "lit/directives/if-defined.js"; | ||||
| import { until } from "lit/directives/until.js"; | ||||
|  | ||||
| import { | ||||
|     CapabilitiesEnum, | ||||
|     DeniedActionEnum, | ||||
|     Flow, | ||||
|     FlowDesignationEnum, | ||||
|     FlowsApi, | ||||
|     LayoutEnum, | ||||
|     PolicyEngineMode, | ||||
| } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-flow-form") | ||||
| export class FlowForm extends ModelForm<Flow, string> { | ||||
|     loadInstance(pk: string): Promise<Flow> { | ||||
|         return new FlowsApi(DEFAULT_CONFIG).flowsInstancesRetrieve({ | ||||
|             slug: pk, | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     getSuccessMessage(): string { | ||||
|         if (this.instance) { | ||||
|             return t`Successfully updated flow.`; | ||||
|         } else { | ||||
|             return t`Successfully created flow.`; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @property({ type: Boolean }) | ||||
|     clearBackground = false; | ||||
|  | ||||
|     send = async (data: Flow): Promise<void | Flow> => { | ||||
|         let flow: Flow; | ||||
|         if (this.instance) { | ||||
|             flow = await new FlowsApi(DEFAULT_CONFIG).flowsInstancesUpdate({ | ||||
|                 slug: this.instance.slug, | ||||
|                 flowRequest: data, | ||||
|             }); | ||||
|         } else { | ||||
|             flow = await new FlowsApi(DEFAULT_CONFIG).flowsInstancesCreate({ | ||||
|                 flowRequest: data, | ||||
|             }); | ||||
|         } | ||||
|         const c = await config(); | ||||
|         if (c.capabilities.includes(CapabilitiesEnum.SaveMedia)) { | ||||
|             const icon = this.getFormFiles()["background"]; | ||||
|             if (icon || this.clearBackground) { | ||||
|                 await new FlowsApi(DEFAULT_CONFIG).flowsInstancesSetBackgroundCreate({ | ||||
|                     slug: flow.slug, | ||||
|                     file: icon, | ||||
|                     clear: this.clearBackground, | ||||
|                 }); | ||||
|             } | ||||
|         } else { | ||||
|             await new FlowsApi(DEFAULT_CONFIG).flowsInstancesSetBackgroundUrlCreate({ | ||||
|                 slug: flow.slug, | ||||
|                 filePathRequest: { | ||||
|                     url: data.background || "", | ||||
|                 }, | ||||
|             }); | ||||
|         } | ||||
|         return flow; | ||||
|     }; | ||||
|  | ||||
|     renderDesignations(): TemplateResult { | ||||
|         return html` | ||||
|             <option | ||||
|                 value=${FlowDesignationEnum.Authentication} | ||||
|                 ?selected=${this.instance?.designation === FlowDesignationEnum.Authentication} | ||||
|             > | ||||
|                 ${DesignationToLabel(FlowDesignationEnum.Authentication)} | ||||
|             </option> | ||||
|             <option | ||||
|                 value=${FlowDesignationEnum.Authorization} | ||||
|                 ?selected=${this.instance?.designation === FlowDesignationEnum.Authorization} | ||||
|             > | ||||
|                 ${DesignationToLabel(FlowDesignationEnum.Authorization)} | ||||
|             </option> | ||||
|             <option | ||||
|                 value=${FlowDesignationEnum.Enrollment} | ||||
|                 ?selected=${this.instance?.designation === FlowDesignationEnum.Enrollment} | ||||
|             > | ||||
|                 ${DesignationToLabel(FlowDesignationEnum.Enrollment)} | ||||
|             </option> | ||||
|             <option | ||||
|                 value=${FlowDesignationEnum.Invalidation} | ||||
|                 ?selected=${this.instance?.designation === FlowDesignationEnum.Invalidation} | ||||
|             > | ||||
|                 ${DesignationToLabel(FlowDesignationEnum.Invalidation)} | ||||
|             </option> | ||||
|             <option | ||||
|                 value=${FlowDesignationEnum.Recovery} | ||||
|                 ?selected=${this.instance?.designation === FlowDesignationEnum.Recovery} | ||||
|             > | ||||
|                 ${DesignationToLabel(FlowDesignationEnum.Recovery)} | ||||
|             </option> | ||||
|             <option | ||||
|                 value=${FlowDesignationEnum.StageConfiguration} | ||||
|                 ?selected=${this.instance?.designation === FlowDesignationEnum.StageConfiguration} | ||||
|             > | ||||
|                 ${DesignationToLabel(FlowDesignationEnum.StageConfiguration)} | ||||
|             </option> | ||||
|             <option | ||||
|                 value=${FlowDesignationEnum.Unenrollment} | ||||
|                 ?selected=${this.instance?.designation === FlowDesignationEnum.Unenrollment} | ||||
|             > | ||||
|                 ${DesignationToLabel(FlowDesignationEnum.Unenrollment)} | ||||
|             </option> | ||||
|         `; | ||||
|     } | ||||
|  | ||||
|     renderDeniedAction(): TemplateResult { | ||||
|         return html` <option | ||||
|                 value=${DeniedActionEnum.MessageContinue} | ||||
|                 ?selected=${this.instance?.deniedAction === DeniedActionEnum.MessageContinue} | ||||
|             > | ||||
|                 ${t`MESSAGE_CONTINUE will follow the ?next parameter if set, otherwise show a message.`} | ||||
|             </option> | ||||
|             <option | ||||
|                 value=${DeniedActionEnum.Continue} | ||||
|                 ?selected=${this.instance?.deniedAction === DeniedActionEnum.Continue} | ||||
|             > | ||||
|                 ${t`CONTINUE will either follow the ?next parameter or redirect to the default interface.`} | ||||
|             </option> | ||||
|             <option | ||||
|                 value=${DeniedActionEnum.Message} | ||||
|                 ?selected=${this.instance?.deniedAction === DeniedActionEnum.Message} | ||||
|             > | ||||
|                 ${t`MESSAGE will notify the user the flow isn't applicable.`} | ||||
|             </option>`; | ||||
|     } | ||||
|  | ||||
|     renderLayout(): TemplateResult { | ||||
|         return html` | ||||
|             <option | ||||
|                 value=${LayoutEnum.Stacked} | ||||
|                 ?selected=${this.instance?.layout === LayoutEnum.Stacked} | ||||
|             > | ||||
|                 ${LayoutToLabel(LayoutEnum.Stacked)} | ||||
|             </option> | ||||
|             <option | ||||
|                 value=${LayoutEnum.ContentLeft} | ||||
|                 ?selected=${this.instance?.layout === LayoutEnum.ContentLeft} | ||||
|             > | ||||
|                 ${LayoutToLabel(LayoutEnum.ContentLeft)} | ||||
|             </option> | ||||
|             <option | ||||
|                 value=${LayoutEnum.ContentRight} | ||||
|                 ?selected=${this.instance?.layout === LayoutEnum.ContentRight} | ||||
|             > | ||||
|                 ${LayoutToLabel(LayoutEnum.ContentRight)} | ||||
|             </option> | ||||
|             <option | ||||
|                 value=${LayoutEnum.SidebarLeft} | ||||
|                 ?selected=${this.instance?.layout === LayoutEnum.SidebarLeft} | ||||
|             > | ||||
|                 ${LayoutToLabel(LayoutEnum.SidebarLeft)} | ||||
|             </option> | ||||
|             <option | ||||
|                 value=${LayoutEnum.SidebarRight} | ||||
|                 ?selected=${this.instance?.layout === LayoutEnum.SidebarRight} | ||||
|             > | ||||
|                 ${LayoutToLabel(LayoutEnum.SidebarRight)} | ||||
|             </option> | ||||
|         `; | ||||
|     } | ||||
|  | ||||
|     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="${ifDefined(this.instance?.name)}" | ||||
|                     class="pf-c-form-control" | ||||
|                     required | ||||
|                 /> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-element-horizontal label=${t`Title`} ?required=${true} name="title"> | ||||
|                 <input | ||||
|                     type="text" | ||||
|                     value="${ifDefined(this.instance?.title)}" | ||||
|                     class="pf-c-form-control" | ||||
|                     required | ||||
|                 /> | ||||
|                 <p class="pf-c-form__helper-text">${t`Shown as the Title in Flow pages.`}</p> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-element-horizontal label=${t`Slug`} ?required=${true} name="slug"> | ||||
|                 <input | ||||
|                     type="text" | ||||
|                     value="${ifDefined(this.instance?.slug)}" | ||||
|                     class="pf-c-form-control" | ||||
|                     required | ||||
|                 /> | ||||
|                 <p class="pf-c-form__helper-text">${t`Visible in the URL.`}</p> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-element-horizontal | ||||
|                 label=${t`Policy engine mode`} | ||||
|                 ?required=${true} | ||||
|                 name="policyEngineMode" | ||||
|             > | ||||
|                 <select class="pf-c-form-control"> | ||||
|                     <option | ||||
|                         value=${PolicyEngineMode.Any} | ||||
|                         ?selected=${this.instance?.policyEngineMode === PolicyEngineMode.Any} | ||||
|                     > | ||||
|                         ${t`ANY, any policy must match to grant access.`} | ||||
|                     </option> | ||||
|                     <option | ||||
|                         value=${PolicyEngineMode.All} | ||||
|                         ?selected=${this.instance?.policyEngineMode === PolicyEngineMode.All} | ||||
|                     > | ||||
|                         ${t`ALL, all policies must match to grant access.`} | ||||
|                     </option> | ||||
|                 </select> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-element-horizontal | ||||
|                 label=${t`Designation`} | ||||
|                 ?required=${true} | ||||
|                 name="designation" | ||||
|             > | ||||
|                 <select class="pf-c-form-control"> | ||||
|                     <option value="" ?selected=${this.instance?.designation === undefined}> | ||||
|                         --------- | ||||
|                     </option> | ||||
|                     ${this.renderDesignations()} | ||||
|                 </select> | ||||
|                 <p class="pf-c-form__helper-text"> | ||||
|                     ${t`Decides what this Flow is used for. For example, the Authentication flow is redirect to when an un-authenticated user visits authentik.`} | ||||
|                 </p> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-element-horizontal | ||||
|                 label=${t`Denied action`} | ||||
|                 ?required=${true} | ||||
|                 name="deniedAction" | ||||
|             > | ||||
|                 <select class="pf-c-form-control"> | ||||
|                     ${this.renderDeniedAction()} | ||||
|                 </select> | ||||
|                 <p class="pf-c-form__helper-text"> | ||||
|                     ${t`Decides the response when a policy denies access to this flow for a user.`} | ||||
|                 </p> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-element-horizontal label=${t`Layout`} ?required=${true} name="layout"> | ||||
|                 <select class="pf-c-form-control"> | ||||
|                     ${this.renderLayout()} | ||||
|                 </select> | ||||
|             </ak-form-element-horizontal> | ||||
|             ${until( | ||||
|                 config().then((c) => { | ||||
|                     if (c.capabilities.includes(CapabilitiesEnum.SaveMedia)) { | ||||
|                         return html`<ak-form-element-horizontal | ||||
|                                 label=${t`Background`} | ||||
|                                 name="background" | ||||
|                             > | ||||
|                                 <input type="file" value="" class="pf-c-form-control" /> | ||||
|                                 ${this.instance?.background | ||||
|                                     ? html` | ||||
|                                           <p class="pf-c-form__helper-text"> | ||||
|                                               ${t`Currently set to:`} ${this.instance?.background} | ||||
|                                           </p> | ||||
|                                       ` | ||||
|                                     : html``} | ||||
|                                 <p class="pf-c-form__helper-text"> | ||||
|                                     ${t`Background shown during execution.`} | ||||
|                                 </p> | ||||
|                             </ak-form-element-horizontal> | ||||
|                             ${this.instance?.background | ||||
|                                 ? html` | ||||
|                                       <ak-form-element-horizontal> | ||||
|                                           <div class="pf-c-check"> | ||||
|                                               <input | ||||
|                                                   type="checkbox" | ||||
|                                                   class="pf-c-check__input" | ||||
|                                                   @change=${(ev: Event) => { | ||||
|                                                       const target = ev.target as HTMLInputElement; | ||||
|                                                       this.clearBackground = target.checked; | ||||
|                                                   }} | ||||
|                                               /> | ||||
|                                               <label class="pf-c-check__label"> | ||||
|                                                   ${t`Clear background image`} | ||||
|                                               </label> | ||||
|                                           </div> | ||||
|                                           <p class="pf-c-form__helper-text"> | ||||
|                                               ${t`Delete currently set background image.`} | ||||
|                                           </p> | ||||
|                                       </ak-form-element-horizontal> | ||||
|                                   ` | ||||
|                                 : html``}`; | ||||
|                     } | ||||
|                     return html`<ak-form-element-horizontal | ||||
|                         label=${t`Background`} | ||||
|                         name="background" | ||||
|                     > | ||||
|                         <input | ||||
|                             type="text" | ||||
|                             value="${first(this.instance?.background, "")}" | ||||
|                             class="pf-c-form-control" | ||||
|                         /> | ||||
|                         <p class="pf-c-form__helper-text"> | ||||
|                             ${t`Background shown during execution.`} | ||||
|                         </p> | ||||
|                     </ak-form-element-horizontal>`; | ||||
|                 }), | ||||
|             )} | ||||
|             <ak-form-element-horizontal name="compatibilityMode"> | ||||
|                 <div class="pf-c-check"> | ||||
|                     <input | ||||
|                         type="checkbox" | ||||
|                         class="pf-c-check__input" | ||||
|                         ?checked=${first(this.instance?.compatibilityMode, false)} | ||||
|                     /> | ||||
|                     <label class="pf-c-check__label"> ${t`Compatibility mode`} </label> | ||||
|                 </div> | ||||
|                 <p class="pf-c-form__helper-text"> | ||||
|                     ${t`Enable compatibility mode, increases compatibility with password managers on mobile devices.`} | ||||
|                 </p> | ||||
|             </ak-form-element-horizontal> | ||||
|         </form>`; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										40
									
								
								web/src/admin/flows/FlowImportForm.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								web/src/admin/flows/FlowImportForm.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,40 @@ | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { SentryIgnoredError } from "@goauthentik/common/errors"; | ||||
| import { Form } from "@goauthentik/elements/forms/Form"; | ||||
| import "@goauthentik/elements/forms/HorizontalFormElement"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { TemplateResult, html } from "lit"; | ||||
| import { customElement } from "lit/decorators.js"; | ||||
|  | ||||
| import { Flow, FlowsApi } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-flow-import-form") | ||||
| export class FlowImportForm extends Form<Flow> { | ||||
|     getSuccessMessage(): string { | ||||
|         return t`Successfully imported flow.`; | ||||
|     } | ||||
|  | ||||
|     // eslint-disable-next-line | ||||
|     send = (data: Flow): Promise<void> => { | ||||
|         const file = this.getFormFiles()["flow"]; | ||||
|         if (!file) { | ||||
|             throw new SentryIgnoredError("No form data"); | ||||
|         } | ||||
|         return new FlowsApi(DEFAULT_CONFIG).flowsInstancesImportFlowCreate({ | ||||
|             file: file, | ||||
|         }); | ||||
|     }; | ||||
|  | ||||
|     renderForm(): TemplateResult { | ||||
|         return html`<form class="pf-c-form pf-m-horizontal"> | ||||
|             <ak-form-element-horizontal label=${t`Flow`} name="flow"> | ||||
|                 <input type="file" value="" class="pf-c-form-control" /> | ||||
|                 <p class="pf-c-form__helper-text"> | ||||
|                     ${t`.yaml files, which can be found on goauthentik.io and can be exported by authentik.`} | ||||
|                 </p> | ||||
|             </ak-form-element-horizontal> | ||||
|         </form>`; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										170
									
								
								web/src/admin/flows/FlowListPage.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										170
									
								
								web/src/admin/flows/FlowListPage.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,170 @@ | ||||
| import "@goauthentik/admin/flows/FlowForm"; | ||||
| import "@goauthentik/admin/flows/FlowImportForm"; | ||||
| import { DesignationToLabel } from "@goauthentik/admin/flows/utils"; | ||||
| import { AndNext, DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { uiConfig } from "@goauthentik/common/ui/config"; | ||||
| import { groupBy } from "@goauthentik/common/utils"; | ||||
| import "@goauthentik/elements/buttons/SpinnerButton"; | ||||
| import "@goauthentik/elements/forms/ConfirmationForm"; | ||||
| import "@goauthentik/elements/forms/DeleteBulkForm"; | ||||
| import "@goauthentik/elements/forms/ModalForm"; | ||||
| import { PaginatedResponse } from "@goauthentik/elements/table/Table"; | ||||
| import { TableColumn } from "@goauthentik/elements/table/Table"; | ||||
| import { TablePage } from "@goauthentik/elements/table/TablePage"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { TemplateResult, html } from "lit"; | ||||
| import { customElement, property } from "lit/decorators.js"; | ||||
|  | ||||
| import { Flow, FlowsApi } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-flow-list") | ||||
| export class FlowListPage extends TablePage<Flow> { | ||||
|     searchEnabled(): boolean { | ||||
|         return true; | ||||
|     } | ||||
|     pageTitle(): string { | ||||
|         return t`Flows`; | ||||
|     } | ||||
|     pageDescription(): string { | ||||
|         return t`Flows describe a chain of Stages to authenticate, enroll or recover a user. Stages are chosen based on policies applied to them.`; | ||||
|     } | ||||
|     pageIcon(): string { | ||||
|         return "pf-icon pf-icon-process-automation"; | ||||
|     } | ||||
|  | ||||
|     checkbox = true; | ||||
|  | ||||
|     @property() | ||||
|     order = "slug"; | ||||
|  | ||||
|     async apiEndpoint(page: number): Promise<PaginatedResponse<Flow>> { | ||||
|         return new FlowsApi(DEFAULT_CONFIG).flowsInstancesList({ | ||||
|             ordering: this.order, | ||||
|             page: page, | ||||
|             pageSize: (await uiConfig()).pagination.perPage, | ||||
|             search: this.search || "", | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     groupBy(items: Flow[]): [string, Flow[]][] { | ||||
|         return groupBy(items, (flow) => { | ||||
|             if (!flow.designation) { | ||||
|                 return ""; | ||||
|             } | ||||
|             return DesignationToLabel(flow.designation); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     columns(): TableColumn[] { | ||||
|         return [ | ||||
|             new TableColumn(t`Identifier`, "slug"), | ||||
|             new TableColumn(t`Name`, "name"), | ||||
|             new TableColumn(t`Stages`), | ||||
|             new TableColumn(t`Policies`), | ||||
|             new TableColumn(t`Actions`), | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     renderToolbarSelected(): TemplateResult { | ||||
|         const disabled = this.selectedElements.length < 1; | ||||
|         return html`<ak-forms-delete-bulk | ||||
|             objectLabel=${t`Flow(s)`} | ||||
|             .objects=${this.selectedElements} | ||||
|             .usedBy=${(item: Flow) => { | ||||
|                 return new FlowsApi(DEFAULT_CONFIG).flowsInstancesUsedByList({ | ||||
|                     slug: item.slug, | ||||
|                 }); | ||||
|             }} | ||||
|             .delete=${(item: Flow) => { | ||||
|                 return new FlowsApi(DEFAULT_CONFIG).flowsInstancesDestroy({ | ||||
|                     slug: item.slug, | ||||
|                 }); | ||||
|             }} | ||||
|         > | ||||
|             <button ?disabled=${disabled} slot="trigger" class="pf-c-button pf-m-danger"> | ||||
|                 ${t`Delete`} | ||||
|             </button> | ||||
|         </ak-forms-delete-bulk>`; | ||||
|     } | ||||
|  | ||||
|     row(item: Flow): TemplateResult[] { | ||||
|         return [ | ||||
|             html`<div> | ||||
|                 <div> | ||||
|                     <a href="#/flow/flows/${item.slug}"> | ||||
|                         <code>${item.slug}</code> | ||||
|                     </a> | ||||
|                 </div> | ||||
|                 <small>${item.title}</small> | ||||
|             </div>`, | ||||
|             html`${item.name}`, | ||||
|             html`${Array.from(item.stages || []).length}`, | ||||
|             html`${Array.from(item.policies || []).length}`, | ||||
|             html` <ak-forms-modal> | ||||
|                     <span slot="submit"> ${t`Update`} </span> | ||||
|                     <span slot="header"> ${t`Update Flow`} </span> | ||||
|                     <ak-flow-form slot="form" .instancePk=${item.slug}> </ak-flow-form> | ||||
|                     <button slot="trigger" class="pf-c-button pf-m-plain"> | ||||
|                         <i class="fas fa-edit"></i> | ||||
|                     </button> | ||||
|                 </ak-forms-modal> | ||||
|                 <button | ||||
|                     class="pf-c-button pf-m-plain" | ||||
|                     @click=${() => { | ||||
|                         const finalURL = `${window.location.origin}/if/flow/${item.slug}/${AndNext( | ||||
|                             `${window.location.pathname}#${window.location.hash}`, | ||||
|                         )}`; | ||||
|                         window.open(finalURL, "_blank"); | ||||
|                     }} | ||||
|                 > | ||||
|                     <i class="fas fa-play"></i> | ||||
|                 </button> | ||||
|                 <a class="pf-c-button pf-m-plain" href=${item.exportUrl}> | ||||
|                     <i class="fas fa-download"></i> | ||||
|                 </a>`, | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     renderObjectCreate(): TemplateResult { | ||||
|         return html` | ||||
|             <ak-forms-modal> | ||||
|                 <span slot="submit"> ${t`Create`} </span> | ||||
|                 <span slot="header"> ${t`Create Flow`} </span> | ||||
|                 <ak-flow-form slot="form"> </ak-flow-form> | ||||
|                 <button slot="trigger" class="pf-c-button pf-m-primary">${t`Create`}</button> | ||||
|             </ak-forms-modal> | ||||
|             <ak-forms-modal> | ||||
|                 <span slot="submit"> ${t`Import`} </span> | ||||
|                 <span slot="header"> ${t`Import Flow`} </span> | ||||
|                 <ak-flow-import-form slot="form"> </ak-flow-import-form> | ||||
|                 <button slot="trigger" class="pf-c-button pf-m-primary">${t`Import`}</button> | ||||
|             </ak-forms-modal> | ||||
|         `; | ||||
|     } | ||||
|  | ||||
|     renderToolbar(): TemplateResult { | ||||
|         return html` | ||||
|             ${super.renderToolbar()} | ||||
|             <ak-forms-confirm | ||||
|                 successMessage=${t`Successfully cleared flow cache`} | ||||
|                 errorMessage=${t`Failed to delete flow cache`} | ||||
|                 action=${t`Clear cache`} | ||||
|                 .onConfirm=${() => { | ||||
|                     return new FlowsApi(DEFAULT_CONFIG).flowsInstancesCacheClearCreate(); | ||||
|                 }} | ||||
|             > | ||||
|                 <span slot="header"> ${t`Clear Flow cache`} </span> | ||||
|                 <p slot="body"> | ||||
|                     ${t`Are you sure you want to clear the flow cache? | ||||
|                     This will cause all flows to be re-evaluated on their next usage.`} | ||||
|                 </p> | ||||
|                 <button slot="trigger" class="pf-c-button pf-m-secondary" type="button"> | ||||
|                     ${t`Clear cache`} | ||||
|                 </button> | ||||
|                 <div slot="modal"></div> | ||||
|             </ak-forms-confirm> | ||||
|         `; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										254
									
								
								web/src/admin/flows/FlowViewPage.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										254
									
								
								web/src/admin/flows/FlowViewPage.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,254 @@ | ||||
| import "@goauthentik/admin/flows/BoundStagesList"; | ||||
| import "@goauthentik/admin/flows/FlowDiagram"; | ||||
| import "@goauthentik/admin/flows/FlowForm"; | ||||
| import "@goauthentik/admin/policies/BoundPoliciesList"; | ||||
| import { AndNext, DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { AKElement } from "@goauthentik/elements/Base"; | ||||
| import "@goauthentik/elements/PageHeader"; | ||||
| import "@goauthentik/elements/Tabs"; | ||||
| import "@goauthentik/elements/buttons/SpinnerButton"; | ||||
| import "@goauthentik/elements/events/ObjectChangelog"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { CSSResult, TemplateResult, css, html } from "lit"; | ||||
| import { customElement, property } from "lit/decorators.js"; | ||||
|  | ||||
| import AKGlobal from "@goauthentik/common/styles/authentik.css"; | ||||
| import PFButton from "@patternfly/patternfly/components/Button/button.css"; | ||||
| import PFCard from "@patternfly/patternfly/components/Card/card.css"; | ||||
| import PFContent from "@patternfly/patternfly/components/Content/content.css"; | ||||
| import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css"; | ||||
| import PFPage from "@patternfly/patternfly/components/Page/page.css"; | ||||
| import PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css"; | ||||
| import PFBase from "@patternfly/patternfly/patternfly-base.css"; | ||||
|  | ||||
| import { Flow, FlowsApi, ResponseError } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-flow-view") | ||||
| export class FlowViewPage extends AKElement { | ||||
|     @property() | ||||
|     set flowSlug(value: string) { | ||||
|         new FlowsApi(DEFAULT_CONFIG) | ||||
|             .flowsInstancesRetrieve({ | ||||
|                 slug: value, | ||||
|             }) | ||||
|             .then((flow) => { | ||||
|                 this.flow = flow; | ||||
|             }); | ||||
|     } | ||||
|  | ||||
|     @property({ attribute: false }) | ||||
|     flow!: Flow; | ||||
|  | ||||
|     static get styles(): CSSResult[] { | ||||
|         return [ | ||||
|             PFBase, | ||||
|             PFPage, | ||||
|             PFDescriptionList, | ||||
|             PFButton, | ||||
|             PFCard, | ||||
|             PFContent, | ||||
|             PFGrid, | ||||
|             AKGlobal, | ||||
|         ].concat( | ||||
|             css` | ||||
|                 img.pf-icon { | ||||
|                     max-height: 24px; | ||||
|                 } | ||||
|                 ak-tabs { | ||||
|                     height: 100%; | ||||
|                 } | ||||
|             `, | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     render(): TemplateResult { | ||||
|         if (!this.flow) { | ||||
|             return html``; | ||||
|         } | ||||
|         return html`<ak-page-header | ||||
|                 icon="pf-icon pf-icon-process-automation" | ||||
|                 header=${this.flow.name} | ||||
|                 description=${this.flow.title} | ||||
|             > | ||||
|             </ak-page-header> | ||||
|             <ak-tabs> | ||||
|                 <div | ||||
|                     slot="page-overview" | ||||
|                     data-tab-title="${t`Flow Overview`}" | ||||
|                     class="pf-c-page__main-section pf-m-no-padding-mobile" | ||||
|                 > | ||||
|                     <div class="pf-l-grid pf-m-gutter"> | ||||
|                         <div | ||||
|                             class="pf-c-card pf-l-grid__item pf-m-12-col pf-m-2-col-on-xl pf-m-2-col-on-2xl" | ||||
|                         > | ||||
|                             <div class="pf-c-card__title">${t`Related`}</div> | ||||
|                             <div class="pf-c-card__body"> | ||||
|                                 <dl class="pf-c-description-list"> | ||||
|                                     <div class="pf-c-description-list__group"> | ||||
|                                         <dt class="pf-c-description-list__term"> | ||||
|                                             <span class="pf-c-description-list__text" | ||||
|                                                 >${t`Edit`}</span | ||||
|                                             > | ||||
|                                         </dt> | ||||
|                                         <dd class="pf-c-description-list__description"> | ||||
|                                             <div class="pf-c-description-list__text"> | ||||
|                                                 <ak-forms-modal> | ||||
|                                                     <span slot="submit"> ${t`Update`} </span> | ||||
|                                                     <span slot="header"> ${t`Update Flow`} </span> | ||||
|                                                     <ak-flow-form | ||||
|                                                         slot="form" | ||||
|                                                         .instancePk=${this.flow.slug} | ||||
|                                                     > | ||||
|                                                     </ak-flow-form> | ||||
|                                                     <button | ||||
|                                                         slot="trigger" | ||||
|                                                         class="pf-c-button pf-m-secondary" | ||||
|                                                     > | ||||
|                                                         ${t`Edit`} | ||||
|                                                     </button> | ||||
|                                                 </ak-forms-modal> | ||||
|                                             </div> | ||||
|                                         </dd> | ||||
|                                         <dt class="pf-c-description-list__term"> | ||||
|                                             <span class="pf-c-description-list__text" | ||||
|                                                 >${t`Execute flow`}</span | ||||
|                                             > | ||||
|                                         </dt> | ||||
|                                         <dd class="pf-c-description-list__description"> | ||||
|                                             <div class="pf-c-description-list__text"> | ||||
|                                                 <button | ||||
|                                                     class="pf-c-button pf-m-primary" | ||||
|                                                     @click=${() => { | ||||
|                                                         const finalURL = `${ | ||||
|                                                             window.location.origin | ||||
|                                                         }/if/flow/${this.flow.slug}/${AndNext( | ||||
|                                                             `${window.location.pathname}#${window.location.hash}`, | ||||
|                                                         )}`; | ||||
|                                                         window.open(finalURL, "_blank"); | ||||
|                                                     }} | ||||
|                                                 > | ||||
|                                                     ${t`Normal`} | ||||
|                                                 </button> | ||||
|                                                 <button | ||||
|                                                     class="pf-c-button pf-m-secondary" | ||||
|                                                     @click=${() => { | ||||
|                                                         new FlowsApi(DEFAULT_CONFIG) | ||||
|                                                             .flowsInstancesExecuteRetrieve({ | ||||
|                                                                 slug: this.flow.slug, | ||||
|                                                             }) | ||||
|                                                             .then((link) => { | ||||
|                                                                 const finalURL = `${ | ||||
|                                                                     link.link | ||||
|                                                                 }${AndNext( | ||||
|                                                                     `${window.location.pathname}#${window.location.hash}`, | ||||
|                                                                 )}`; | ||||
|                                                                 window.open(finalURL, "_blank"); | ||||
|                                                             }); | ||||
|                                                     }} | ||||
|                                                 > | ||||
|                                                     ${t`with current user`} | ||||
|                                                 </button> | ||||
|                                                 <button | ||||
|                                                     class="pf-c-button pf-m-secondary" | ||||
|                                                     @click=${() => { | ||||
|                                                         new FlowsApi(DEFAULT_CONFIG) | ||||
|                                                             .flowsInstancesExecuteRetrieve({ | ||||
|                                                                 slug: this.flow.slug, | ||||
|                                                             }) | ||||
|                                                             .then((link) => { | ||||
|                                                                 const finalURL = `${ | ||||
|                                                                     link.link | ||||
|                                                                 }?${encodeURI( | ||||
|                                                                     `inspector&next=/#${window.location.hash}`, | ||||
|                                                                 )}`; | ||||
|                                                                 window.open(finalURL, "_blank"); | ||||
|                                                             }) | ||||
|                                                             .catch((exc: ResponseError) => { | ||||
|                                                                 // This request can return a HTTP 400 when a flow | ||||
|                                                                 // is not applicable. | ||||
|                                                                 window.open( | ||||
|                                                                     exc.response.url, | ||||
|                                                                     "_blank", | ||||
|                                                                 ); | ||||
|                                                             }); | ||||
|                                                     }} | ||||
|                                                 > | ||||
|                                                     ${t`with inspector`} | ||||
|                                                 </button> | ||||
|                                             </div> | ||||
|                                         </dd> | ||||
|                                         <dt class="pf-c-description-list__term"> | ||||
|                                             <span class="pf-c-description-list__text" | ||||
|                                                 >${t`Export flow`}</span | ||||
|                                             > | ||||
|                                         </dt> | ||||
|                                         <dd class="pf-c-description-list__description"> | ||||
|                                             <div class="pf-c-description-list__text"> | ||||
|                                                 <a | ||||
|                                                     class="pf-c-button pf-m-secondary" | ||||
|                                                     href=${this.flow.exportUrl} | ||||
|                                                 > | ||||
|                                                     ${t`Export`} | ||||
|                                                 </a> | ||||
|                                             </div> | ||||
|                                         </dd> | ||||
|                                     </div> | ||||
|                                 </dl> | ||||
|                             </div> | ||||
|                         </div> | ||||
|                         <div | ||||
|                             class="pf-c-card pf-l-grid__item pf-m-12-col pf-m-10-col-on-xl pf-m-10-col-on-2xl" | ||||
|                         > | ||||
|                             <div class="pf-c-card__title">${t`Diagram`}</div> | ||||
|                             <div class="pf-c-card"> | ||||
|                                 <div class="pf-c-card__body"> | ||||
|                                     <ak-flow-diagram flowSlug=${this.flow.slug}> </ak-flow-diagram> | ||||
|                                 </div> | ||||
|                             </div> | ||||
|                         </div> | ||||
|                         <div | ||||
|                             class="pf-c-card pf-l-grid__item pf-m-12-col pf-m-12-col-on-xl pf-m-12-col-on-2xl" | ||||
|                         > | ||||
|                             <div class="pf-c-card__title">${t`Changelog`}</div> | ||||
|                             <div class="pf-c-card__body"> | ||||
|                                 <ak-object-changelog | ||||
|                                     targetModelPk=${this.flow.pk || ""} | ||||
|                                     targetModelApp="authentik_flows" | ||||
|                                     targetModelName="flow" | ||||
|                                 > | ||||
|                                 </ak-object-changelog> | ||||
|                             </div> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                 </div> | ||||
|                 <div | ||||
|                     slot="page-stage-bindings" | ||||
|                     data-tab-title="${t`Stage Bindings`}" | ||||
|                     class="pf-c-page__main-section pf-m-no-padding-mobile" | ||||
|                 > | ||||
|                     <div class="pf-c-card"> | ||||
|                         <div class="pf-c-card__body"> | ||||
|                             <ak-bound-stages-list .target=${this.flow.pk}> </ak-bound-stages-list> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                 </div> | ||||
|                 <div | ||||
|                     slot="page-policy-bindings" | ||||
|                     data-tab-title="${t`Policy / Group / User Bindings`}" | ||||
|                     class="pf-c-page__main-section pf-m-no-padding-mobile" | ||||
|                 > | ||||
|                     <div class="pf-c-card"> | ||||
|                         <div class="pf-c-card__title"> | ||||
|                             ${t`These bindings control which users can access this flow.`} | ||||
|                         </div> | ||||
|                         <div class="pf-c-card__body"> | ||||
|                             <ak-bound-policies-list .target=${this.flow.policybindingmodelPtrId}> | ||||
|                             </ak-bound-policies-list> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </ak-tabs>`; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										225
									
								
								web/src/admin/flows/StageBindingForm.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										225
									
								
								web/src/admin/flows/StageBindingForm.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,225 @@ | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { first, groupBy } from "@goauthentik/common/utils"; | ||||
| import "@goauthentik/elements/forms/HorizontalFormElement"; | ||||
| import { ModelForm } from "@goauthentik/elements/forms/ModelForm"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { TemplateResult, html } from "lit"; | ||||
| import { customElement, property } from "lit/decorators.js"; | ||||
| import { ifDefined } from "lit/directives/if-defined.js"; | ||||
| import { until } from "lit/directives/until.js"; | ||||
|  | ||||
| import { | ||||
|     FlowStageBinding, | ||||
|     FlowsApi, | ||||
|     InvalidResponseActionEnum, | ||||
|     PolicyEngineMode, | ||||
|     Stage, | ||||
|     StagesApi, | ||||
| } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-stage-binding-form") | ||||
| export class StageBindingForm extends ModelForm<FlowStageBinding, string> { | ||||
|     loadInstance(pk: string): Promise<FlowStageBinding> { | ||||
|         return new FlowsApi(DEFAULT_CONFIG).flowsBindingsRetrieve({ | ||||
|             fsbUuid: pk, | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     @property() | ||||
|     targetPk?: string; | ||||
|  | ||||
|     getSuccessMessage(): string { | ||||
|         if (this.instance) { | ||||
|             return t`Successfully updated binding.`; | ||||
|         } else { | ||||
|             return t`Successfully created binding.`; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     send = (data: FlowStageBinding): Promise<FlowStageBinding> => { | ||||
|         if (this.instance) { | ||||
|             return new FlowsApi(DEFAULT_CONFIG).flowsBindingsUpdate({ | ||||
|                 fsbUuid: this.instance.pk || "", | ||||
|                 flowStageBindingRequest: data, | ||||
|             }); | ||||
|         } else { | ||||
|             return new FlowsApi(DEFAULT_CONFIG).flowsBindingsCreate({ | ||||
|                 flowStageBindingRequest: data, | ||||
|             }); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     groupStages(stages: Stage[]): TemplateResult { | ||||
|         return html` | ||||
|             <option value="">---------</option> | ||||
|             ${groupBy<Stage>(stages, (s) => s.verboseName || "").map(([group, stages]) => { | ||||
|                 return html`<optgroup label=${group}> | ||||
|                     ${stages.map((stage) => { | ||||
|                         const selected = this.instance?.stage === stage.pk; | ||||
|                         return html`<option ?selected=${selected} value=${ifDefined(stage.pk)}> | ||||
|                             ${stage.name} | ||||
|                         </option>`; | ||||
|                     })} | ||||
|                 </optgroup>`; | ||||
|             })} | ||||
|         `; | ||||
|     } | ||||
|  | ||||
|     getOrder(): Promise<number> { | ||||
|         if (this.instance) { | ||||
|             return Promise.resolve(this.instance.order); | ||||
|         } | ||||
|         return new FlowsApi(DEFAULT_CONFIG) | ||||
|             .flowsBindingsList({ | ||||
|                 target: this.targetPk || "", | ||||
|             }) | ||||
|             .then((bindings) => { | ||||
|                 const orders = bindings.results.map((binding) => binding.order); | ||||
|                 if (orders.length < 1) { | ||||
|                     return 0; | ||||
|                 } | ||||
|                 return Math.max(...orders) + 1; | ||||
|             }); | ||||
|     } | ||||
|  | ||||
|     renderTarget(): TemplateResult { | ||||
|         if (this.instance?.target || this.targetPk) { | ||||
|             return html` | ||||
|                 <input | ||||
|                     required | ||||
|                     name="target" | ||||
|                     type="hidden" | ||||
|                     value=${ifDefined(this.instance?.target || this.targetPk)} | ||||
|                 /> | ||||
|             `; | ||||
|         } | ||||
|         return html`<ak-form-element-horizontal label=${t`Target`} ?required=${true} name="target"> | ||||
|             <select class="pf-c-form-control"> | ||||
|                 ${until( | ||||
|                     new FlowsApi(DEFAULT_CONFIG) | ||||
|                         .flowsInstancesList({ | ||||
|                             ordering: "slug", | ||||
|                         }) | ||||
|                         .then((flows) => { | ||||
|                             return flows.results.map((flow) => { | ||||
|                                 // No ?selected check here, as this input isn't shown on update forms | ||||
|                                 return html`<option value=${ifDefined(flow.pk)}> | ||||
|                                     ${flow.name} (${flow.slug}) | ||||
|                                 </option>`; | ||||
|                             }); | ||||
|                         }), | ||||
|                     html`<option>${t`Loading...`}</option>`, | ||||
|                 )} | ||||
|             </select> | ||||
|         </ak-form-element-horizontal>`; | ||||
|     } | ||||
|  | ||||
|     renderForm(): TemplateResult { | ||||
|         return html`<form class="pf-c-form pf-m-horizontal"> | ||||
|             ${this.renderTarget()} | ||||
|             <ak-form-element-horizontal label=${t`Stage`} ?required=${true} name="stage"> | ||||
|                 <select class="pf-c-form-control"> | ||||
|                     ${until( | ||||
|                         new StagesApi(DEFAULT_CONFIG) | ||||
|                             .stagesAllList({ | ||||
|                                 ordering: "name", | ||||
|                             }) | ||||
|                             .then((stages) => { | ||||
|                                 return this.groupStages(stages.results); | ||||
|                             }), | ||||
|                         html`<option>${t`Loading...`}</option>`, | ||||
|                     )} | ||||
|                 </select> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-element-horizontal label=${t`Order`} ?required=${true} name="order"> | ||||
|                 <!-- @ts-ignore --> | ||||
|                 <input | ||||
|                     type="number" | ||||
|                     value="${until(this.getOrder())}" | ||||
|                     class="pf-c-form-control" | ||||
|                     required | ||||
|                 /> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-element-horizontal name="evaluateOnPlan"> | ||||
|                 <div class="pf-c-check"> | ||||
|                     <input | ||||
|                         type="checkbox" | ||||
|                         class="pf-c-check__input" | ||||
|                         ?checked=${first(this.instance?.evaluateOnPlan, true)} | ||||
|                     /> | ||||
|                     <label class="pf-c-check__label"> ${t`Evaluate on plan`} </label> | ||||
|                 </div> | ||||
|                 <p class="pf-c-form__helper-text"> | ||||
|                     ${t`Evaluate policies during the Flow planning process. Disable this for input-based policies. Should be used in conjunction with 'Re-evaluate policies', as with both options disabled, policies are **not** evaluated.`} | ||||
|                 </p> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-element-horizontal name="reEvaluatePolicies"> | ||||
|                 <div class="pf-c-check"> | ||||
|                     <input | ||||
|                         type="checkbox" | ||||
|                         class="pf-c-check__input" | ||||
|                         ?checked=${first(this.instance?.reEvaluatePolicies, false)} | ||||
|                     /> | ||||
|                     <label class="pf-c-check__label"> ${t`Re-evaluate policies`} </label> | ||||
|                 </div> | ||||
|                 <p class="pf-c-form__helper-text"> | ||||
|                     ${t`Evaluate policies before the Stage is present to the user.`} | ||||
|                 </p> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-element-horizontal | ||||
|                 label=${t`Invalid response action`} | ||||
|                 ?required=${true} | ||||
|                 name="invalidResponseAction" | ||||
|             > | ||||
|                 <select class="pf-c-form-control"> | ||||
|                     <option | ||||
|                         value=${InvalidResponseActionEnum.Retry} | ||||
|                         ?selected=${this.instance?.invalidResponseAction === | ||||
|                         InvalidResponseActionEnum.Retry} | ||||
|                     > | ||||
|                         ${t`RETRY returns the error message and a similar challenge to the executor.`} | ||||
|                     </option> | ||||
|                     <option | ||||
|                         value=${InvalidResponseActionEnum.Restart} | ||||
|                         ?selected=${this.instance?.invalidResponseAction === | ||||
|                         InvalidResponseActionEnum.Restart} | ||||
|                     > | ||||
|                         ${t`RESTART restarts the flow from the beginning.`} | ||||
|                     </option> | ||||
|                     <option | ||||
|                         value=${InvalidResponseActionEnum.RestartWithContext} | ||||
|                         ?selected=${this.instance?.invalidResponseAction === | ||||
|                         InvalidResponseActionEnum.RestartWithContext} | ||||
|                     > | ||||
|                         ${t`RESTART_WITH_CONTEXT restarts the flow from the beginning, while keeping the flow context.`} | ||||
|                     </option> | ||||
|                 </select> | ||||
|                 <p class="pf-c-form__helper-text"> | ||||
|                     ${t`Configure how the flow executor should handle an invalid response to a challenge.`} | ||||
|                 </p> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-element-horizontal | ||||
|                 label=${t`Policy engine mode`} | ||||
|                 ?required=${true} | ||||
|                 name="policyEngineMode" | ||||
|             > | ||||
|                 <select class="pf-c-form-control"> | ||||
|                     <option | ||||
|                         value=${PolicyEngineMode.Any} | ||||
|                         ?selected=${this.instance?.policyEngineMode === PolicyEngineMode.Any} | ||||
|                     > | ||||
|                         ${t`ANY, any policy must match to include this stage access.`} | ||||
|                     </option> | ||||
|                     <option | ||||
|                         value=${PolicyEngineMode.All} | ||||
|                         ?selected=${this.instance?.policyEngineMode === PolicyEngineMode.All} | ||||
|                     > | ||||
|                         ${t`ALL, all policies must match to include this stage access.`} | ||||
|                     </option> | ||||
|                 </select> | ||||
|             </ak-form-element-horizontal> | ||||
|         </form>`; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										37
									
								
								web/src/admin/flows/utils.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								web/src/admin/flows/utils.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,37 @@ | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { FlowDesignationEnum, LayoutEnum } from "@goauthentik/api"; | ||||
|  | ||||
| export function DesignationToLabel(designation: FlowDesignationEnum): string { | ||||
|     switch (designation) { | ||||
|         case FlowDesignationEnum.Authentication: | ||||
|             return t`Authentication`; | ||||
|         case FlowDesignationEnum.Authorization: | ||||
|             return t`Authorization`; | ||||
|         case FlowDesignationEnum.Enrollment: | ||||
|             return t`Enrollment`; | ||||
|         case FlowDesignationEnum.Invalidation: | ||||
|             return t`Invalidation`; | ||||
|         case FlowDesignationEnum.Recovery: | ||||
|             return t`Recovery`; | ||||
|         case FlowDesignationEnum.StageConfiguration: | ||||
|             return t`Stage Configuration`; | ||||
|         case FlowDesignationEnum.Unenrollment: | ||||
|             return t`Unenrollment`; | ||||
|     } | ||||
| } | ||||
|  | ||||
| export function LayoutToLabel(layout: LayoutEnum): string { | ||||
|     switch (layout) { | ||||
|         case LayoutEnum.Stacked: | ||||
|             return t`Stacked`; | ||||
|         case LayoutEnum.ContentLeft: | ||||
|             return t`Content left`; | ||||
|         case LayoutEnum.ContentRight: | ||||
|             return t`Content right`; | ||||
|         case LayoutEnum.SidebarLeft: | ||||
|             return t`Sidebar left`; | ||||
|         case LayoutEnum.SidebarRight: | ||||
|             return t`Sidebar right`; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										174
									
								
								web/src/admin/groups/GroupForm.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										174
									
								
								web/src/admin/groups/GroupForm.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,174 @@ | ||||
| import "@goauthentik/admin/groups/MemberSelectModal"; | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { first } from "@goauthentik/common/utils"; | ||||
| import "@goauthentik/elements/CodeMirror"; | ||||
| import "@goauthentik/elements/chips/Chip"; | ||||
| import "@goauthentik/elements/chips/ChipGroup"; | ||||
| import "@goauthentik/elements/forms/HorizontalFormElement"; | ||||
| import { ModelForm } from "@goauthentik/elements/forms/ModelForm"; | ||||
| import { UserOption } from "@goauthentik/elements/user/utils"; | ||||
| import YAML from "yaml"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { CSSResult, TemplateResult, css, html } from "lit"; | ||||
| import { customElement } from "lit/decorators.js"; | ||||
| import { ifDefined } from "lit/directives/if-defined.js"; | ||||
| import { until } from "lit/directives/until.js"; | ||||
|  | ||||
| import { CoreApi, Group, User } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-group-form") | ||||
| export class GroupForm extends ModelForm<Group, string> { | ||||
|     static get styles(): CSSResult[] { | ||||
|         return super.styles.concat(css` | ||||
|             .pf-c-button.pf-m-control { | ||||
|                 height: 100%; | ||||
|             } | ||||
|             .pf-c-form-control { | ||||
|                 height: auto !important; | ||||
|             } | ||||
|         `); | ||||
|     } | ||||
|  | ||||
|     loadInstance(pk: string): Promise<Group> { | ||||
|         return new CoreApi(DEFAULT_CONFIG).coreGroupsRetrieve({ | ||||
|             groupUuid: pk, | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     getSuccessMessage(): string { | ||||
|         if (this.instance) { | ||||
|             return t`Successfully updated group.`; | ||||
|         } else { | ||||
|             return t`Successfully created group.`; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     send = (data: Group): Promise<Group> => { | ||||
|         if (this.instance?.pk) { | ||||
|             return new CoreApi(DEFAULT_CONFIG).coreGroupsUpdate({ | ||||
|                 groupUuid: this.instance.pk, | ||||
|                 groupRequest: data, | ||||
|             }); | ||||
|         } else { | ||||
|             data.users = Array.from(this.instance?.users || []); | ||||
|             return new CoreApi(DEFAULT_CONFIG).coreGroupsCreate({ | ||||
|                 groupRequest: 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="${ifDefined(this.instance?.name)}" | ||||
|                     class="pf-c-form-control" | ||||
|                     required | ||||
|                 /> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-element-horizontal name="isSuperuser"> | ||||
|                 <div class="pf-c-check"> | ||||
|                     <input | ||||
|                         type="checkbox" | ||||
|                         class="pf-c-check__input" | ||||
|                         ?checked=${first(this.instance?.isSuperuser, false)} | ||||
|                     /> | ||||
|                     <label class="pf-c-check__label"> ${t`Is superuser`} </label> | ||||
|                 </div> | ||||
|                 <p class="pf-c-form__helper-text"> | ||||
|                     ${t`Users added to this group will be superusers.`} | ||||
|                 </p> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-element-horizontal label=${t`Parent`} name="parent"> | ||||
|                 <select class="pf-c-form-control"> | ||||
|                     <option value="" ?selected=${this.instance?.parent === undefined}> | ||||
|                         --------- | ||||
|                     </option> | ||||
|                     ${until( | ||||
|                         new CoreApi(DEFAULT_CONFIG).coreGroupsList({}).then((groups) => { | ||||
|                             return groups.results.map((group) => { | ||||
|                                 return html`<option | ||||
|                                     value=${ifDefined(group.pk)} | ||||
|                                     ?selected=${this.instance?.parent === group.pk} | ||||
|                                 > | ||||
|                                     ${group.name} | ||||
|                                 </option>`; | ||||
|                             }); | ||||
|                         }), | ||||
|                         html`<option>${t`Loading...`}</option>`, | ||||
|                     )} | ||||
|                 </select> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-element-horizontal label=${t`Members`} name="users"> | ||||
|                 <div class="pf-c-input-group"> | ||||
|                     <ak-group-member-select-table | ||||
|                         .confirm=${(items: User[]) => { | ||||
|                             // Because the model only has the IDs, map the user list to IDs | ||||
|                             const ids = items.map((u) => u.pk || 0); | ||||
|                             if (!this.instance) this.instance = {} as Group; | ||||
|                             this.instance.users = Array.from(this.instance?.users || []).concat( | ||||
|                                 ids, | ||||
|                             ); | ||||
|                             this.requestUpdate(); | ||||
|                             return Promise.resolve(); | ||||
|                         }} | ||||
|                     > | ||||
|                         <button slot="trigger" class="pf-c-button pf-m-control" type="button"> | ||||
|                             <i class="fas fa-plus" aria-hidden="true"></i> | ||||
|                         </button> | ||||
|                     </ak-group-member-select-table> | ||||
|                     <div class="pf-c-form-control"> | ||||
|                         <ak-chip-group> | ||||
|                             ${until( | ||||
|                                 new CoreApi(DEFAULT_CONFIG) | ||||
|                                     .coreUsersList({ | ||||
|                                         ordering: "username", | ||||
|                                     }) | ||||
|                                     .then((users) => { | ||||
|                                         return users.results.map((user) => { | ||||
|                                             const selected = Array.from( | ||||
|                                                 this.instance?.users || [], | ||||
|                                             ).some((su) => { | ||||
|                                                 return su == user.pk; | ||||
|                                             }); | ||||
|                                             if (!selected) return; | ||||
|                                             return html`<ak-chip | ||||
|                                                 .removable=${true} | ||||
|                                                 value=${ifDefined(user.pk)} | ||||
|                                                 @remove=${() => { | ||||
|                                                     if (!this.instance) return; | ||||
|                                                     const users = Array.from( | ||||
|                                                         this.instance?.users || [], | ||||
|                                                     ); | ||||
|                                                     const idx = users.indexOf(user.pk || 0); | ||||
|                                                     users.splice(idx, 1); | ||||
|                                                     this.instance.users = users; | ||||
|                                                     this.requestUpdate(); | ||||
|                                                 }} | ||||
|                                             > | ||||
|                                                 ${UserOption(user)} | ||||
|                                             </ak-chip>`; | ||||
|                                         }); | ||||
|                                     }), | ||||
|                                 html`<option>${t`Loading...`}</option>`, | ||||
|                             )} | ||||
|                         </ak-chip-group> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-element-horizontal label=${t`Attributes`} ?required=${true} name="attributes"> | ||||
|                 <ak-codemirror | ||||
|                     mode="yaml" | ||||
|                     value="${YAML.stringify(first(this.instance?.attributes, {}))}" | ||||
|                 > | ||||
|                 </ak-codemirror> | ||||
|                 <p class="pf-c-form__helper-text"> | ||||
|                     ${t`Set custom attributes using YAML or JSON.`} | ||||
|                 </p> | ||||
|             </ak-form-element-horizontal> | ||||
|         </form>`; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										108
									
								
								web/src/admin/groups/GroupListPage.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								web/src/admin/groups/GroupListPage.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,108 @@ | ||||
| import "@goauthentik/admin/groups/GroupForm"; | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { uiConfig } from "@goauthentik/common/ui/config"; | ||||
| import { PFColor } from "@goauthentik/elements/Label"; | ||||
| import "@goauthentik/elements/buttons/SpinnerButton"; | ||||
| import "@goauthentik/elements/forms/DeleteBulkForm"; | ||||
| import "@goauthentik/elements/forms/ModalForm"; | ||||
| import { PaginatedResponse } from "@goauthentik/elements/table/Table"; | ||||
| import { TableColumn } from "@goauthentik/elements/table/Table"; | ||||
| import { TablePage } from "@goauthentik/elements/table/TablePage"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { TemplateResult, html } from "lit"; | ||||
| import { customElement, property } from "lit/decorators.js"; | ||||
|  | ||||
| import { CoreApi, Group } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-group-list") | ||||
| export class GroupListPage extends TablePage<Group> { | ||||
|     checkbox = true; | ||||
|     searchEnabled(): boolean { | ||||
|         return true; | ||||
|     } | ||||
|     pageTitle(): string { | ||||
|         return t`Groups`; | ||||
|     } | ||||
|     pageDescription(): string { | ||||
|         return t`Group users together and give them permissions based on the membership.`; | ||||
|     } | ||||
|     pageIcon(): string { | ||||
|         return "pf-icon pf-icon-users"; | ||||
|     } | ||||
|  | ||||
|     @property() | ||||
|     order = "name"; | ||||
|  | ||||
|     async apiEndpoint(page: number): Promise<PaginatedResponse<Group>> { | ||||
|         return new CoreApi(DEFAULT_CONFIG).coreGroupsList({ | ||||
|             ordering: this.order, | ||||
|             page: page, | ||||
|             pageSize: (await uiConfig()).pagination.perPage, | ||||
|             search: this.search || "", | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     columns(): TableColumn[] { | ||||
|         return [ | ||||
|             new TableColumn(t`Name`, "name"), | ||||
|             new TableColumn(t`Parent`, "parent"), | ||||
|             new TableColumn(t`Members`), | ||||
|             new TableColumn(t`Superuser privileges?`), | ||||
|             new TableColumn(t`Actions`), | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     renderToolbarSelected(): TemplateResult { | ||||
|         const disabled = this.selectedElements.length < 1; | ||||
|         return html`<ak-forms-delete-bulk | ||||
|             objectLabel=${t`Group(s)`} | ||||
|             .objects=${this.selectedElements} | ||||
|             .usedBy=${(item: Group) => { | ||||
|                 return new CoreApi(DEFAULT_CONFIG).coreGroupsUsedByList({ | ||||
|                     groupUuid: item.pk, | ||||
|                 }); | ||||
|             }} | ||||
|             .delete=${(item: Group) => { | ||||
|                 return new CoreApi(DEFAULT_CONFIG).coreGroupsDestroy({ | ||||
|                     groupUuid: item.pk, | ||||
|                 }); | ||||
|             }} | ||||
|         > | ||||
|             <button ?disabled=${disabled} slot="trigger" class="pf-c-button pf-m-danger"> | ||||
|                 ${t`Delete`} | ||||
|             </button> | ||||
|         </ak-forms-delete-bulk>`; | ||||
|     } | ||||
|  | ||||
|     row(item: Group): TemplateResult[] { | ||||
|         return [ | ||||
|             html`<a href="#/identity/groups/${item.pk}">${item.name}</a>`, | ||||
|             html`${item.parentName || t`-`}`, | ||||
|             html`${Array.from(item.users || []).length}`, | ||||
|             html` <ak-label color=${item.isSuperuser ? PFColor.Green : PFColor.Grey}> | ||||
|                 ${item.isSuperuser ? t`Yes` : t`No`} | ||||
|             </ak-label>`, | ||||
|             html` <ak-forms-modal> | ||||
|                 <span slot="submit"> ${t`Update`} </span> | ||||
|                 <span slot="header"> ${t`Update Group`} </span> | ||||
|                 <ak-group-form slot="form" .instancePk=${item.pk}> </ak-group-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 Group`} </span> | ||||
|                 <ak-group-form slot="form"> </ak-group-form> | ||||
|                 <button slot="trigger" class="pf-c-button pf-m-primary">${t`Create`}</button> | ||||
|             </ak-forms-modal> | ||||
|         `; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										169
									
								
								web/src/admin/groups/GroupViewPage.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										169
									
								
								web/src/admin/groups/GroupViewPage.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,169 @@ | ||||
| import "@goauthentik/admin/groups/GroupForm"; | ||||
| import "@goauthentik/admin/users/RelatedUserList"; | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { EVENT_REFRESH } from "@goauthentik/common/constants"; | ||||
| import { AKElement } from "@goauthentik/elements/Base"; | ||||
| import "@goauthentik/elements/CodeMirror"; | ||||
| import { PFColor } from "@goauthentik/elements/Label"; | ||||
| import "@goauthentik/elements/PageHeader"; | ||||
| import "@goauthentik/elements/Tabs"; | ||||
| import "@goauthentik/elements/buttons/ActionButton"; | ||||
| import "@goauthentik/elements/buttons/SpinnerButton"; | ||||
| import "@goauthentik/elements/events/ObjectChangelog"; | ||||
| import "@goauthentik/elements/forms/ModalForm"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { CSSResult, TemplateResult, html } from "lit"; | ||||
| import { customElement, property } from "lit/decorators.js"; | ||||
|  | ||||
| import AKGlobal from "@goauthentik/common/styles/authentik.css"; | ||||
| import PFButton from "@patternfly/patternfly/components/Button/button.css"; | ||||
| import PFCard from "@patternfly/patternfly/components/Card/card.css"; | ||||
| import PFContent from "@patternfly/patternfly/components/Content/content.css"; | ||||
| import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css"; | ||||
| import PFPage from "@patternfly/patternfly/components/Page/page.css"; | ||||
| import PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css"; | ||||
| import PFBase from "@patternfly/patternfly/patternfly-base.css"; | ||||
| import PFDisplay from "@patternfly/patternfly/utilities/Display/display.css"; | ||||
| import PFFlex from "@patternfly/patternfly/utilities/Flex/flex.css"; | ||||
| import PFSizing from "@patternfly/patternfly/utilities/Sizing/sizing.css"; | ||||
|  | ||||
| import { CoreApi, Group } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-group-view") | ||||
| export class GroupViewPage extends AKElement { | ||||
|     @property({ type: String }) | ||||
|     set groupId(id: string) { | ||||
|         new CoreApi(DEFAULT_CONFIG) | ||||
|             .coreGroupsRetrieve({ | ||||
|                 groupUuid: id, | ||||
|             }) | ||||
|             .then((group) => { | ||||
|                 this.group = group; | ||||
|             }); | ||||
|     } | ||||
|  | ||||
|     @property({ attribute: false }) | ||||
|     group?: Group; | ||||
|  | ||||
|     static get styles(): CSSResult[] { | ||||
|         return [ | ||||
|             PFBase, | ||||
|             PFPage, | ||||
|             PFFlex, | ||||
|             PFButton, | ||||
|             PFDisplay, | ||||
|             PFGrid, | ||||
|             PFContent, | ||||
|             PFCard, | ||||
|             PFDescriptionList, | ||||
|             PFSizing, | ||||
|             AKGlobal, | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     constructor() { | ||||
|         super(); | ||||
|         this.addEventListener(EVENT_REFRESH, () => { | ||||
|             if (!this.group?.pk) return; | ||||
|             this.groupId = this.group?.pk; | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     render(): TemplateResult { | ||||
|         return html`<ak-page-header | ||||
|                 icon="pf-icon pf-icon-users" | ||||
|                 header=${t`Group ${this.group?.name || ""}`} | ||||
|                 description=${this.group?.name || ""} | ||||
|             > | ||||
|             </ak-page-header> | ||||
|             ${this.renderBody()}`; | ||||
|     } | ||||
|  | ||||
|     renderBody(): TemplateResult { | ||||
|         if (!this.group) { | ||||
|             return html``; | ||||
|         } | ||||
|         return html`<ak-tabs> | ||||
|             <section | ||||
|                 slot="page-overview" | ||||
|                 data-tab-title="${t`Overview`}" | ||||
|                 class="pf-c-page__main-section pf-m-no-padding-mobile" | ||||
|             > | ||||
|                 <div class="pf-l-grid pf-m-gutter"> | ||||
|                     <div | ||||
|                         class="pf-c-card pf-l-grid__item pf-m-12-col pf-m-3-col-on-xl pf-m-3-col-on-2xl" | ||||
|                     > | ||||
|                         <div class="pf-c-card__title">${t`Group Info`}</div> | ||||
|                         <div class="pf-c-card__body"> | ||||
|                             <dl class="pf-c-description-list pf-m-2-col"> | ||||
|                                 <div class="pf-c-description-list__group"> | ||||
|                                     <dt class="pf-c-description-list__term"> | ||||
|                                         <span class="pf-c-description-list__text">${t`Name`}</span> | ||||
|                                     </dt> | ||||
|                                     <dd class="pf-c-description-list__description"> | ||||
|                                         <div class="pf-c-description-list__text"> | ||||
|                                             ${this.group.name} | ||||
|                                         </div> | ||||
|                                     </dd> | ||||
|                                 </div> | ||||
|                                 <div class="pf-c-description-list__group"> | ||||
|                                     <dt class="pf-c-description-list__term"> | ||||
|                                         <span class="pf-c-description-list__text" | ||||
|                                             >${t`Superuser`}</span | ||||
|                                         > | ||||
|                                     </dt> | ||||
|                                     <dd class="pf-c-description-list__description"> | ||||
|                                         <div class="pf-c-description-list__text"> | ||||
|                                             <ak-label | ||||
|                                                 color=${this.group.isSuperuser | ||||
|                                                     ? PFColor.Green | ||||
|                                                     : PFColor.Orange} | ||||
|                                             ></ak-label> | ||||
|                                         </div> | ||||
|                                     </dd> | ||||
|                                 </div> | ||||
|                             </dl> | ||||
|                         </div> | ||||
|                         <div class="pf-c-card__footer"> | ||||
|                             <ak-forms-modal> | ||||
|                                 <span slot="submit"> ${t`Update`} </span> | ||||
|                                 <span slot="header"> ${t`Update Group`} </span> | ||||
|                                 <ak-group-form slot="form" .instancePk=${this.group.pk}> | ||||
|                                 </ak-group-form> | ||||
|                                 <button slot="trigger" class="pf-m-primary pf-c-button"> | ||||
|                                     ${t`Edit`} | ||||
|                                 </button> | ||||
|                             </ak-forms-modal> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                     <div | ||||
|                         class="pf-c-card pf-l-grid__item pf-m-12-col pf-m-12-col-on-xl pf-m-12-col-on-2xl" | ||||
|                     > | ||||
|                         <div class="pf-c-card__title">${t`Changelog`}</div> | ||||
|                         <div class="pf-c-card__body"> | ||||
|                             <ak-object-changelog | ||||
|                                 targetModelPk=${this.group.pk} | ||||
|                                 targetModelApp="authentik_core" | ||||
|                                 targetModelName="group" | ||||
|                             > | ||||
|                             </ak-object-changelog> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </section> | ||||
|             <section | ||||
|                 slot="page-users" | ||||
|                 data-tab-title="${t`Users`}" | ||||
|                 class="pf-c-page__main-section pf-m-no-padding-mobile" | ||||
|             > | ||||
|                 <div class="pf-c-card"> | ||||
|                     <div class="pf-c-card__body"> | ||||
|                         <ak-user-related-list groupUuid=${this.group.pk}> </ak-user-related-list> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </section> | ||||
|         </ak-tabs>`; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										95
									
								
								web/src/admin/groups/MemberSelectModal.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										95
									
								
								web/src/admin/groups/MemberSelectModal.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,95 @@ | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { uiConfig } from "@goauthentik/common/ui/config"; | ||||
| import { first } from "@goauthentik/common/utils"; | ||||
| import { PFColor } from "@goauthentik/elements/Label"; | ||||
| import "@goauthentik/elements/buttons/SpinnerButton"; | ||||
| import { PaginatedResponse } from "@goauthentik/elements/table/Table"; | ||||
| import { TableColumn } from "@goauthentik/elements/table/Table"; | ||||
| import { TableModal } from "@goauthentik/elements/table/TableModal"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { TemplateResult, html } from "lit"; | ||||
| import { customElement, property } from "lit/decorators.js"; | ||||
|  | ||||
| import { CoreApi, User } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-group-member-select-table") | ||||
| export class MemberSelectTable extends TableModal<User> { | ||||
|     checkbox = true; | ||||
|     checkboxChip = true; | ||||
|  | ||||
|     searchEnabled(): boolean { | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     @property() | ||||
|     confirm!: (selectedItems: User[]) => Promise<unknown>; | ||||
|  | ||||
|     order = "username"; | ||||
|  | ||||
|     async apiEndpoint(page: number): Promise<PaginatedResponse<User>> { | ||||
|         return new CoreApi(DEFAULT_CONFIG).coreUsersList({ | ||||
|             ordering: this.order, | ||||
|             page: page, | ||||
|             pageSize: (await uiConfig()).pagination.perPage, | ||||
|             search: this.search || "", | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     columns(): TableColumn[] { | ||||
|         return [ | ||||
|             new TableColumn(t`Name`, "username"), | ||||
|             new TableColumn(t`Active`, "active"), | ||||
|             new TableColumn(t`Last login`, "last_login"), | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     row(item: User): TemplateResult[] { | ||||
|         return [ | ||||
|             html`<div> | ||||
|                 <div>${item.username}</div> | ||||
|                 <small>${item.name}</small> | ||||
|             </div>`, | ||||
|             html` <ak-label color=${item.isActive ? PFColor.Green : PFColor.Orange}> | ||||
|                 ${item.isActive ? t`Yes` : t`No`} | ||||
|             </ak-label>`, | ||||
|             html`${first(item.lastLogin?.toLocaleString(), t`-`)}`, | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     renderSelectedChip(item: User): TemplateResult { | ||||
|         return html`${item.username}`; | ||||
|     } | ||||
|  | ||||
|     renderModalInner(): TemplateResult { | ||||
|         return html`<section class="pf-c-modal-box__header pf-c-page__main-section pf-m-light"> | ||||
|                 <div class="pf-c-content"> | ||||
|                     <h1 class="pf-c-title pf-m-2xl">${t`Select users to add`}</h1> | ||||
|                 </div> | ||||
|             </section> | ||||
|             <section class="pf-c-modal-box__body pf-c-page__main-section pf-m-light"> | ||||
|                 ${this.renderTable()} | ||||
|             </section> | ||||
|             <footer class="pf-c-modal-box__footer"> | ||||
|                 <ak-spinner-button | ||||
|                     .callAction=${() => { | ||||
|                         return this.confirm(this.selectedElements).then(() => { | ||||
|                             this.open = false; | ||||
|                         }); | ||||
|                     }} | ||||
|                     class="pf-m-primary" | ||||
|                 > | ||||
|                     ${t`Add`} </ak-spinner-button | ||||
|                 >  | ||||
|                 <ak-spinner-button | ||||
|                     .callAction=${async () => { | ||||
|                         this.open = false; | ||||
|                     }} | ||||
|                     class="pf-m-secondary" | ||||
|                 > | ||||
|                     ${t`Cancel`} | ||||
|                 </ak-spinner-button> | ||||
|             </footer>`; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										89
									
								
								web/src/admin/groups/RelatedGroupList.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								web/src/admin/groups/RelatedGroupList.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,89 @@ | ||||
| import "@goauthentik/admin/groups/GroupForm"; | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { uiConfig } from "@goauthentik/common/ui/config"; | ||||
| import { PFColor } from "@goauthentik/elements/Label"; | ||||
| import "@goauthentik/elements/buttons/SpinnerButton"; | ||||
| import "@goauthentik/elements/forms/DeleteBulkForm"; | ||||
| import "@goauthentik/elements/forms/ModalForm"; | ||||
| import { PaginatedResponse } from "@goauthentik/elements/table/Table"; | ||||
| import { Table, TableColumn } from "@goauthentik/elements/table/Table"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { TemplateResult, html } from "lit"; | ||||
| import { customElement, property } from "lit/decorators.js"; | ||||
|  | ||||
| import { CoreApi, Group } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-group-related-list") | ||||
| export class RelatedGroupList extends Table<Group> { | ||||
|     checkbox = true; | ||||
|     searchEnabled(): boolean { | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     @property() | ||||
|     order = "name"; | ||||
|  | ||||
|     @property({ type: Number }) | ||||
|     targetUser?: number; | ||||
|  | ||||
|     async apiEndpoint(page: number): Promise<PaginatedResponse<Group>> { | ||||
|         return new CoreApi(DEFAULT_CONFIG).coreGroupsList({ | ||||
|             ordering: this.order, | ||||
|             page: page, | ||||
|             pageSize: (await uiConfig()).pagination.perPage, | ||||
|             search: this.search || "", | ||||
|             membersByPk: this.targetUser ? [this.targetUser] : [], | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     columns(): TableColumn[] { | ||||
|         return [ | ||||
|             new TableColumn(t`Name`, "name"), | ||||
|             new TableColumn(t`Parent`, "parent"), | ||||
|             new TableColumn(t`Superuser privileges?`), | ||||
|             new TableColumn(t`Actions`), | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     renderToolbarSelected(): TemplateResult { | ||||
|         const disabled = this.selectedElements.length < 1; | ||||
|         return html`<ak-forms-delete-bulk | ||||
|             objectLabel=${t`Group(s)`} | ||||
|             .objects=${this.selectedElements} | ||||
|             .usedBy=${(item: Group) => { | ||||
|                 return new CoreApi(DEFAULT_CONFIG).coreGroupsUsedByList({ | ||||
|                     groupUuid: item.pk, | ||||
|                 }); | ||||
|             }} | ||||
|             .delete=${(item: Group) => { | ||||
|                 return new CoreApi(DEFAULT_CONFIG).coreGroupsDestroy({ | ||||
|                     groupUuid: item.pk, | ||||
|                 }); | ||||
|             }} | ||||
|         > | ||||
|             <button ?disabled=${disabled} slot="trigger" class="pf-c-button pf-m-danger"> | ||||
|                 ${t`Delete`} | ||||
|             </button> | ||||
|         </ak-forms-delete-bulk>`; | ||||
|     } | ||||
|  | ||||
|     row(item: Group): TemplateResult[] { | ||||
|         return [ | ||||
|             html`<a href="#/identity/groups/${item.pk}">${item.name}</a>`, | ||||
|             html`${item.parentName || t`-`}`, | ||||
|             html`<ak-label color=${item.isSuperuser ? PFColor.Green : PFColor.Grey}> | ||||
|                 ${item.isSuperuser ? t`Yes` : t`No`} | ||||
|             </ak-label>`, | ||||
|             html` <ak-forms-modal> | ||||
|                 <span slot="submit"> ${t`Update`} </span> | ||||
|                 <span slot="header"> ${t`Update Group`} </span> | ||||
|                 <ak-group-form slot="form" .instancePk=${item.pk}> </ak-group-form> | ||||
|                 <button slot="trigger" class="pf-c-button pf-m-plain"> | ||||
|                     <i class="fas fa-edit"></i> | ||||
|                 </button> | ||||
|             </ak-forms-modal>`, | ||||
|         ]; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										96
									
								
								web/src/admin/outposts/OutpostDeploymentModal.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								web/src/admin/outposts/OutpostDeploymentModal.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,96 @@ | ||||
| import { ModalButton } from "@goauthentik/elements/buttons/ModalButton"; | ||||
| import "@goauthentik/elements/buttons/TokenCopyButton"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { TemplateResult, html } from "lit"; | ||||
| import { customElement, property } from "lit/decorators.js"; | ||||
| import { ifDefined } from "lit/directives/if-defined.js"; | ||||
|  | ||||
| import { Outpost, OutpostTypeEnum } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-outpost-deployment-modal") | ||||
| export class OutpostDeploymentModal extends ModalButton { | ||||
|     @property({ attribute: false }) | ||||
|     outpost?: Outpost; | ||||
|  | ||||
|     renderModalInner(): TemplateResult { | ||||
|         return html`<div class="pf-c-modal-box__header"> | ||||
|                 <h1 class="pf-c-title pf-m-2xl">${t`Outpost Deployment Info`}</h1> | ||||
|             </div> | ||||
|             <div class="pf-c-modal-box__body"> | ||||
|                 <p> | ||||
|                     <a | ||||
|                         target="_blank" | ||||
|                         href="https://goauthentik.io/docs/outposts?utm_source=authentik#deploy" | ||||
|                         >${t`View deployment documentation`}</a | ||||
|                     > | ||||
|                 </p> | ||||
|                 <form class="pf-c-form"> | ||||
|                     <div class="pf-c-form__group"> | ||||
|                         <label class="pf-c-form__label"> | ||||
|                             <span class="pf-c-form__label-text">AUTHENTIK_HOST</span> | ||||
|                         </label> | ||||
|                         <input | ||||
|                             class="pf-c-form-control" | ||||
|                             readonly | ||||
|                             type="text" | ||||
|                             value="${document.location.origin}" | ||||
|                         /> | ||||
|                     </div> | ||||
|                     <div class="pf-c-form__group"> | ||||
|                         <label class="pf-c-form__label"> | ||||
|                             <span class="pf-c-form__label-text">AUTHENTIK_TOKEN</span> | ||||
|                         </label> | ||||
|                         <div> | ||||
|                             <ak-token-copy-button | ||||
|                                 class="pf-m-primary" | ||||
|                                 identifier="${ifDefined(this.outpost?.tokenIdentifier)}" | ||||
|                             > | ||||
|                                 ${t`Click to copy token`} | ||||
|                             </ak-token-copy-button> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                     <h3> | ||||
|                         ${t`If your authentik Instance is using a self-signed certificate, set this value.`} | ||||
|                     </h3> | ||||
|                     <div class="pf-c-form__group"> | ||||
|                         <label class="pf-c-form__label"> | ||||
|                             <span class="pf-c-form__label-text">AUTHENTIK_INSECURE</span> | ||||
|                         </label> | ||||
|                         <input class="pf-c-form-control" readonly type="text" value="true" /> | ||||
|                     </div> | ||||
|                     ${this.outpost?.type == OutpostTypeEnum.Proxy | ||||
|                         ? html` | ||||
|                               <h3> | ||||
|                                   ${t`If your authentik_host setting does not match the URL you want to login with, add this setting.`} | ||||
|                               </h3> | ||||
|                               <div class="pf-c-form__group"> | ||||
|                                   <label class="pf-c-form__label"> | ||||
|                                       <span class="pf-c-form__label-text" | ||||
|                                           >AUTHENTIK_HOST_BROWSER</span | ||||
|                                       > | ||||
|                                   </label> | ||||
|                                   <input | ||||
|                                       class="pf-c-form-control" | ||||
|                                       readonly | ||||
|                                       type="text" | ||||
|                                       value="${document.location.origin}" | ||||
|                                   /> | ||||
|                               </div> | ||||
|                           ` | ||||
|                         : html``} | ||||
|                 </form> | ||||
|             </div> | ||||
|             <footer class="pf-c-modal-box__footer pf-m-align-left"> | ||||
|                 <button | ||||
|                     class="pf-c-button pf-m-primary" | ||||
|                     @click=${() => { | ||||
|                         this.open = false; | ||||
|                     }} | ||||
|                 > | ||||
|                     ${t`Close`} | ||||
|                 </button> | ||||
|             </footer>`; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										219
									
								
								web/src/admin/outposts/OutpostForm.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										219
									
								
								web/src/admin/outposts/OutpostForm.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,219 @@ | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import "@goauthentik/elements/CodeMirror"; | ||||
| import "@goauthentik/elements/forms/HorizontalFormElement"; | ||||
| import { ModelForm } from "@goauthentik/elements/forms/ModelForm"; | ||||
| import YAML from "yaml"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { TemplateResult, html } from "lit"; | ||||
| import { customElement, property } from "lit/decorators.js"; | ||||
| import { ifDefined } from "lit/directives/if-defined.js"; | ||||
| import { until } from "lit/directives/until.js"; | ||||
|  | ||||
| import { Outpost, OutpostTypeEnum, OutpostsApi, ProvidersApi } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-outpost-form") | ||||
| export class OutpostForm extends ModelForm<Outpost, string> { | ||||
|     @property() | ||||
|     type: OutpostTypeEnum = OutpostTypeEnum.Proxy; | ||||
|  | ||||
|     @property({ type: Boolean }) | ||||
|     embedded = false; | ||||
|  | ||||
|     loadInstance(pk: string): Promise<Outpost> { | ||||
|         return new OutpostsApi(DEFAULT_CONFIG) | ||||
|             .outpostsInstancesRetrieve({ | ||||
|                 uuid: pk, | ||||
|             }) | ||||
|             .then((o) => { | ||||
|                 this.type = o.type || OutpostTypeEnum.Proxy; | ||||
|                 return o; | ||||
|             }); | ||||
|     } | ||||
|  | ||||
|     getSuccessMessage(): string { | ||||
|         if (this.instance) { | ||||
|             return t`Successfully updated outpost.`; | ||||
|         } else { | ||||
|             return t`Successfully created outpost.`; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     send = (data: Outpost): Promise<Outpost> => { | ||||
|         if (this.instance) { | ||||
|             return new OutpostsApi(DEFAULT_CONFIG).outpostsInstancesUpdate({ | ||||
|                 uuid: this.instance.pk || "", | ||||
|                 outpostRequest: data, | ||||
|             }); | ||||
|         } else { | ||||
|             return new OutpostsApi(DEFAULT_CONFIG).outpostsInstancesCreate({ | ||||
|                 outpostRequest: data, | ||||
|             }); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     renderProviders(): Promise<TemplateResult[]> { | ||||
|         switch (this.type) { | ||||
|             case OutpostTypeEnum.Proxy: | ||||
|                 return new ProvidersApi(DEFAULT_CONFIG) | ||||
|                     .providersProxyList({ | ||||
|                         ordering: "name", | ||||
|                         applicationIsnull: false, | ||||
|                     }) | ||||
|                     .then((providers) => { | ||||
|                         return providers.results.map((provider) => { | ||||
|                             const selected = Array.from(this.instance?.providers || []).some( | ||||
|                                 (sp) => { | ||||
|                                     return sp == provider.pk; | ||||
|                                 }, | ||||
|                             ); | ||||
|                             return html`<option | ||||
|                                 value=${ifDefined(provider.pk)} | ||||
|                                 ?selected=${selected} | ||||
|                             > | ||||
|                                 ${provider.assignedApplicationName} (${provider.externalHost}) | ||||
|                             </option>`; | ||||
|                         }); | ||||
|                     }); | ||||
|             case OutpostTypeEnum.Ldap: | ||||
|                 return new ProvidersApi(DEFAULT_CONFIG) | ||||
|                     .providersLdapList({ | ||||
|                         ordering: "name", | ||||
|                         applicationIsnull: false, | ||||
|                     }) | ||||
|                     .then((providers) => { | ||||
|                         return providers.results.map((provider) => { | ||||
|                             const selected = Array.from(this.instance?.providers || []).some( | ||||
|                                 (sp) => { | ||||
|                                     return sp == provider.pk; | ||||
|                                 }, | ||||
|                             ); | ||||
|                             return html`<option | ||||
|                                 value=${ifDefined(provider.pk)} | ||||
|                                 ?selected=${selected} | ||||
|                             > | ||||
|                                 ${provider.assignedApplicationName} (${provider.name}) | ||||
|                             </option>`; | ||||
|                         }); | ||||
|                     }); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     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="${ifDefined(this.instance?.name)}" | ||||
|                     class="pf-c-form-control" | ||||
|                     required | ||||
|                 /> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-element-horizontal label=${t`Type`} ?required=${true} name="type"> | ||||
|                 <select | ||||
|                     class="pf-c-form-control" | ||||
|                     @change=${(ev: Event) => { | ||||
|                         const target = ev.target as HTMLSelectElement; | ||||
|                         this.type = target.selectedOptions[0].value as OutpostTypeEnum; | ||||
|                     }} | ||||
|                 > | ||||
|                     <option | ||||
|                         value=${OutpostTypeEnum.Proxy} | ||||
|                         ?selected=${this.instance?.type === OutpostTypeEnum.Proxy} | ||||
|                     > | ||||
|                         ${t`Proxy`} | ||||
|                     </option> | ||||
|                     <option | ||||
|                         value=${OutpostTypeEnum.Ldap} | ||||
|                         ?selected=${this.instance?.type === OutpostTypeEnum.Ldap} | ||||
|                     > | ||||
|                         ${t`LDAP`} | ||||
|                     </option> | ||||
|                 </select> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-element-horizontal label=${t`Integration`} name="serviceConnection"> | ||||
|                 <select class="pf-c-form-control"> | ||||
|                     <option value="" ?selected=${this.instance?.serviceConnection === undefined}> | ||||
|                         --------- | ||||
|                     </option> | ||||
|                     ${until( | ||||
|                         new OutpostsApi(DEFAULT_CONFIG) | ||||
|                             .outpostsServiceConnectionsAllList({ | ||||
|                                 ordering: "name", | ||||
|                             }) | ||||
|                             .then((scs) => { | ||||
|                                 return scs.results.map((sc) => { | ||||
|                                     let selected = this.instance?.serviceConnection === sc.pk; | ||||
|                                     if (scs.results.length === 1 && !this.instance) { | ||||
|                                         selected = true; | ||||
|                                     } | ||||
|                                     return html`<option | ||||
|                                         value=${ifDefined(sc.pk)} | ||||
|                                         ?selected=${selected} | ||||
|                                     > | ||||
|                                         ${sc.name} (${sc.verboseName}) | ||||
|                                     </option>`; | ||||
|                                 }); | ||||
|                             }), | ||||
|                         html`<option>${t`Loading...`}</option>`, | ||||
|                     )} | ||||
|                 </select> | ||||
|                 <p class="pf-c-form__helper-text"> | ||||
|                     ${t`Selecting an integration enables the management of the outpost by authentik.`} | ||||
|                 </p> | ||||
|                 <p class="pf-c-form__helper-text"> | ||||
|                     See | ||||
|                     <a | ||||
|                         target="_blank" | ||||
|                         href="https://goauthentik.io/docs/outposts?utm_source=authentik" | ||||
|                         >documentation</a | ||||
|                     >. | ||||
|                 </p> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-element-horizontal | ||||
|                 label=${t`Applications`} | ||||
|                 ?required=${!this.embedded} | ||||
|                 name="providers" | ||||
|             > | ||||
|                 <select class="pf-c-form-control" multiple> | ||||
|                     ${until(this.renderProviders(), html`<option>${t`Loading...`}</option>`)} | ||||
|                 </select> | ||||
|                 <p class="pf-c-form__helper-text"> | ||||
|                     ${t`You can only select providers that match the type of the outpost.`} | ||||
|                 </p> | ||||
|                 <p class="pf-c-form__helper-text"> | ||||
|                     ${t`Hold control/command to select multiple items.`} | ||||
|                 </p> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-element-horizontal label=${t`Configuration`} name="config"> | ||||
|                 <!-- @ts-ignore --> | ||||
|                 <ak-codemirror | ||||
|                     mode="yaml" | ||||
|                     value="${until( | ||||
|                         new OutpostsApi(DEFAULT_CONFIG) | ||||
|                             .outpostsInstancesDefaultSettingsRetrieve() | ||||
|                             .then((config) => { | ||||
|                                 let fc = config.config; | ||||
|                                 if (this.instance) { | ||||
|                                     fc = this.instance.config; | ||||
|                                 } | ||||
|                                 return YAML.stringify(fc); | ||||
|                             }), | ||||
|                     )}" | ||||
|                 ></ak-codemirror> | ||||
|                 <p class="pf-c-form__helper-text"> | ||||
|                     ${t`Set custom attributes using YAML or JSON.`} | ||||
|                 </p> | ||||
|                 <p class="pf-c-form__helper-text"> | ||||
|                     See | ||||
|                     <a | ||||
|                         target="_blank" | ||||
|                         href="https://goauthentik.io/docs/outposts?utm_source=authentik#configuration" | ||||
|                         >documentation</a | ||||
|                     >. | ||||
|                 </p> | ||||
|             </ak-form-element-horizontal> | ||||
|         </form>`; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										60
									
								
								web/src/admin/outposts/OutpostHealth.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								web/src/admin/outposts/OutpostHealth.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,60 @@ | ||||
| import { AKElement } from "@goauthentik/elements/Base"; | ||||
| import { PFColor } from "@goauthentik/elements/Label"; | ||||
| import "@goauthentik/elements/Spinner"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { CSSResult, TemplateResult, css, html } from "lit"; | ||||
| import { customElement, property } from "lit/decorators.js"; | ||||
|  | ||||
| import AKGlobal from "@goauthentik/common/styles/authentik.css"; | ||||
| import PFBase from "@patternfly/patternfly/patternfly-base.css"; | ||||
|  | ||||
| import { OutpostHealth } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-outpost-health") | ||||
| export class OutpostHealthElement extends AKElement { | ||||
|     @property({ attribute: false }) | ||||
|     outpostHealth?: OutpostHealth; | ||||
|  | ||||
|     static get styles(): CSSResult[] { | ||||
|         return [ | ||||
|             PFBase, | ||||
|             AKGlobal, | ||||
|             css` | ||||
|                 li { | ||||
|                     margin: 5px 0; | ||||
|                 } | ||||
|             `, | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     render(): TemplateResult { | ||||
|         if (!this.outpostHealth) { | ||||
|             return html`<ak-spinner></ak-spinner>`; | ||||
|         } | ||||
|         let versionString = this.outpostHealth.version; | ||||
|         if (this.outpostHealth.buildHash) { | ||||
|             versionString = `${versionString} (build ${this.outpostHealth.buildHash.substring( | ||||
|                 0, | ||||
|                 8, | ||||
|             )})`; | ||||
|         } | ||||
|         return html` <ul> | ||||
|             <li> | ||||
|                 <ak-label color=${PFColor.Green}> | ||||
|                     ${t`Last seen: ${this.outpostHealth.lastSeen?.toLocaleTimeString()}`} | ||||
|                 </ak-label> | ||||
|             </li> | ||||
|             <li> | ||||
|                 ${this.outpostHealth.versionOutdated | ||||
|                     ? html`<ak-label color=${PFColor.Red} | ||||
|                           >${t`${this.outpostHealth.version}, should be ${this.outpostHealth.versionShould}`} | ||||
|                       </ak-label>` | ||||
|                     : html`<ak-label color=${PFColor.Green} | ||||
|                           >${t`Version: ${versionString}`} | ||||
|                       </ak-label>`} | ||||
|             </li> | ||||
|         </ul>`; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										68
									
								
								web/src/admin/outposts/OutpostHealthSimple.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								web/src/admin/outposts/OutpostHealthSimple.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,68 @@ | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { EVENT_REFRESH } from "@goauthentik/common/constants"; | ||||
| import { AKElement } from "@goauthentik/elements/Base"; | ||||
| import { PFColor } from "@goauthentik/elements/Label"; | ||||
| import "@goauthentik/elements/Spinner"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { CSSResult, TemplateResult, html } from "lit"; | ||||
| import { customElement, property } from "lit/decorators.js"; | ||||
|  | ||||
| import AKGlobal from "@goauthentik/common/styles/authentik.css"; | ||||
| import PFBase from "@patternfly/patternfly/patternfly-base.css"; | ||||
|  | ||||
| import { OutpostHealth, OutpostsApi } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-outpost-health-simple") | ||||
| export class OutpostHealthSimpleElement extends AKElement { | ||||
|     @property() | ||||
|     outpostId?: string; | ||||
|  | ||||
|     @property({ attribute: false }) | ||||
|     outpostHealth?: OutpostHealth; | ||||
|  | ||||
|     @property({ attribute: false }) | ||||
|     loaded = false; | ||||
|  | ||||
|     @property({ attribute: false }) | ||||
|     showVersion = true; | ||||
|  | ||||
|     static get styles(): CSSResult[] { | ||||
|         return [PFBase, AKGlobal]; | ||||
|     } | ||||
|  | ||||
|     constructor() { | ||||
|         super(); | ||||
|         window.addEventListener(EVENT_REFRESH, () => { | ||||
|             this.outpostHealth = undefined; | ||||
|             this.firstUpdated(); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     firstUpdated(): void { | ||||
|         if (!this.outpostId) return; | ||||
|         new OutpostsApi(DEFAULT_CONFIG) | ||||
|             .outpostsInstancesHealthList({ | ||||
|                 uuid: this.outpostId, | ||||
|             }) | ||||
|             .then((health) => { | ||||
|                 this.loaded = true; | ||||
|                 if (health.length >= 1) { | ||||
|                     this.outpostHealth = health[0]; | ||||
|                 } | ||||
|             }); | ||||
|     } | ||||
|  | ||||
|     render(): TemplateResult { | ||||
|         if (!this.outpostId || !this.loaded) { | ||||
|             return html`<ak-spinner></ak-spinner>`; | ||||
|         } | ||||
|         if (!this.outpostHealth) { | ||||
|             return html`<ak-label color=${PFColor.Grey}>${t`Not available`}</ak-label>`; | ||||
|         } | ||||
|         return html`<ak-label color=${PFColor.Green}> | ||||
|             ${t`Last seen: ${this.outpostHealth.lastSeen?.toLocaleTimeString()}`}</ak-label | ||||
|         >`; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										192
									
								
								web/src/admin/outposts/OutpostListPage.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										192
									
								
								web/src/admin/outposts/OutpostListPage.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,192 @@ | ||||
| import "@goauthentik/admin/outposts/OutpostDeploymentModal"; | ||||
| import "@goauthentik/admin/outposts/OutpostDeploymentModal"; | ||||
| import "@goauthentik/admin/outposts/OutpostForm"; | ||||
| import "@goauthentik/admin/outposts/OutpostHealth"; | ||||
| import "@goauthentik/admin/outposts/OutpostHealthSimple"; | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { uiConfig } from "@goauthentik/common/ui/config"; | ||||
| import { PFSize } from "@goauthentik/elements/Spinner"; | ||||
| import "@goauthentik/elements/buttons/SpinnerButton"; | ||||
| import "@goauthentik/elements/forms/DeleteBulkForm"; | ||||
| import "@goauthentik/elements/forms/ModalForm"; | ||||
| import { PaginatedResponse } from "@goauthentik/elements/table/Table"; | ||||
| import { TableColumn } from "@goauthentik/elements/table/Table"; | ||||
| import { TablePage } from "@goauthentik/elements/table/TablePage"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { CSSResult } from "lit"; | ||||
| import { TemplateResult, html } from "lit"; | ||||
| import { customElement, property } from "lit/decorators.js"; | ||||
| import { ifDefined } from "lit/directives/if-defined.js"; | ||||
| import { until } from "lit/directives/until.js"; | ||||
|  | ||||
| import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css"; | ||||
|  | ||||
| import { Outpost, OutpostTypeEnum, OutpostsApi } from "@goauthentik/api"; | ||||
|  | ||||
| export function TypeToLabel(type?: OutpostTypeEnum): string { | ||||
|     if (!type) return ""; | ||||
|     switch (type) { | ||||
|         case OutpostTypeEnum.Proxy: | ||||
|             return t`Proxy`; | ||||
|         case OutpostTypeEnum.Ldap: | ||||
|             return t`LDAP`; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @customElement("ak-outpost-list") | ||||
| export class OutpostListPage extends TablePage<Outpost> { | ||||
|     expandable = true; | ||||
|  | ||||
|     pageTitle(): string { | ||||
|         return t`Outposts`; | ||||
|     } | ||||
|     pageDescription(): string | undefined { | ||||
|         return t`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; | ||||
|     } | ||||
|     async apiEndpoint(page: number): Promise<PaginatedResponse<Outpost>> { | ||||
|         return new OutpostsApi(DEFAULT_CONFIG).outpostsInstancesList({ | ||||
|             ordering: this.order, | ||||
|             page: page, | ||||
|             pageSize: (await uiConfig()).pagination.perPage, | ||||
|             search: this.search || "", | ||||
|         }); | ||||
|     } | ||||
|     columns(): TableColumn[] { | ||||
|         return [ | ||||
|             new TableColumn(t`Name`, "name"), | ||||
|             new TableColumn(t`Type`, "type"), | ||||
|             new TableColumn(t`Providers`), | ||||
|             new TableColumn(t`Integration`, "service_connection__name"), | ||||
|             new TableColumn(t`Health and Version`), | ||||
|             new TableColumn(t`Actions`), | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     static get styles(): CSSResult[] { | ||||
|         return super.styles.concat(PFDescriptionList); | ||||
|     } | ||||
|  | ||||
|     checkbox = true; | ||||
|  | ||||
|     @property() | ||||
|     order = "name"; | ||||
|  | ||||
|     row(item: Outpost): TemplateResult[] { | ||||
|         return [ | ||||
|             html`<div> | ||||
|                 <div>${item.name}</div> | ||||
|                 ${item.config.authentik_host === "" | ||||
|                     ? html`<i class="pf-icon pf-icon-warning-triangle"></i> | ||||
|                           <small | ||||
|                               >${t`Warning: authentik Domain is not configured, authentication will not work.`}</small | ||||
|                           >` | ||||
|                     : html`<i class="pf-icon pf-icon-ok"></i> | ||||
|                           <small> ${t`Logging in via ${item.config.authentik_host}.`} </small>`} | ||||
|             </div>`, | ||||
|             html`${TypeToLabel(item.type)}`, | ||||
|             html`<ul> | ||||
|                 ${item.providersObj?.map((p) => { | ||||
|                     return html`<li> | ||||
|                         <a href="#/core/providers/${p.pk}">${p.name}</a> | ||||
|                     </li>`; | ||||
|                 })} | ||||
|             </ul>`, | ||||
|             html`${item.serviceConnectionObj?.name || t`No integration active`}`, | ||||
|             html`<ak-outpost-health-simple | ||||
|                 outpostId=${ifDefined(item.pk)} | ||||
|             ></ak-outpost-health-simple>`, | ||||
|             html`<ak-forms-modal> | ||||
|                     <span slot="submit"> ${t`Update`} </span> | ||||
|                     <span slot="header"> ${t`Update Outpost`} </span> | ||||
|                     <ak-outpost-form | ||||
|                         slot="form" | ||||
|                         .instancePk=${item.pk} | ||||
|                         .embedded=${item.managed === "goauthentik.io/outposts/embedded"} | ||||
|                     > | ||||
|                     </ak-outpost-form> | ||||
|                     <button slot="trigger" class="pf-c-button pf-m-plain"> | ||||
|                         <i class="fas fa-edit"></i> | ||||
|                     </button> | ||||
|                 </ak-forms-modal> | ||||
|                 ${item.managed !== "goauthentik.io/outposts/embedded" | ||||
|                     ? html`<ak-outpost-deployment-modal .outpost=${item} size=${PFSize.Medium}> | ||||
|                           <button slot="trigger" class="pf-c-button pf-m-tertiary"> | ||||
|                               ${t`View Deployment Info`} | ||||
|                           </button> | ||||
|                       </ak-outpost-deployment-modal>` | ||||
|                     : html``}`, | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     renderExpanded(item: Outpost): TemplateResult { | ||||
|         return html`<td role="cell" colspan="5"> | ||||
|             <div class="pf-c-table__expandable-row-content"> | ||||
|                 <h3> | ||||
|                     ${t`Detailed health (one instance per column, data is cached so may be out of data)`} | ||||
|                 </h3> | ||||
|                 <dl class="pf-c-description-list pf-m-3-col-on-lg"> | ||||
|                     ${until( | ||||
|                         new OutpostsApi(DEFAULT_CONFIG) | ||||
|                             .outpostsInstancesHealthList({ | ||||
|                                 uuid: item.pk, | ||||
|                             }) | ||||
|                             .then((health) => { | ||||
|                                 return health.map((h) => { | ||||
|                                     return html` <div class="pf-c-description-list__group"> | ||||
|                                         <dd class="pf-c-description-list__description"> | ||||
|                                             <div class="pf-c-description-list__text"> | ||||
|                                                 <ak-outpost-health | ||||
|                                                     .outpostHealth=${h} | ||||
|                                                 ></ak-outpost-health> | ||||
|                                             </div> | ||||
|                                         </dd> | ||||
|                                     </div>`; | ||||
|                                 }); | ||||
|                             }), | ||||
|                     )} | ||||
|                 </dl> | ||||
|             </div> | ||||
|         </td>`; | ||||
|     } | ||||
|  | ||||
|     renderToolbarSelected(): TemplateResult { | ||||
|         const disabled = this.selectedElements.length < 1; | ||||
|         return html`<ak-forms-delete-bulk | ||||
|             objectLabel=${t`Outpost(s)`} | ||||
|             .objects=${this.selectedElements} | ||||
|             .usedBy=${(item: Outpost) => { | ||||
|                 return new OutpostsApi(DEFAULT_CONFIG).outpostsInstancesUsedByList({ | ||||
|                     uuid: item.pk, | ||||
|                 }); | ||||
|             }} | ||||
|             .delete=${(item: Outpost) => { | ||||
|                 return new OutpostsApi(DEFAULT_CONFIG).outpostsInstancesDestroy({ | ||||
|                     uuid: item.pk, | ||||
|                 }); | ||||
|             }} | ||||
|         > | ||||
|             <button ?disabled=${disabled} slot="trigger" class="pf-c-button pf-m-danger"> | ||||
|                 ${t`Delete`} | ||||
|             </button> | ||||
|         </ak-forms-delete-bulk>`; | ||||
|     } | ||||
|  | ||||
|     renderObjectCreate(): TemplateResult { | ||||
|         return html` | ||||
|             <ak-forms-modal> | ||||
|                 <span slot="submit"> ${t`Create`} </span> | ||||
|                 <span slot="header"> ${t`Create Outpost`} </span> | ||||
|                 <ak-outpost-form slot="form"> </ak-outpost-form> | ||||
|                 <button slot="trigger" class="pf-c-button pf-m-primary">${t`Create`}</button> | ||||
|             </ak-forms-modal> | ||||
|         `; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										153
									
								
								web/src/admin/outposts/ServiceConnectionDockerForm.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										153
									
								
								web/src/admin/outposts/ServiceConnectionDockerForm.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,153 @@ | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { first } from "@goauthentik/common/utils"; | ||||
| import "@goauthentik/elements/forms/HorizontalFormElement"; | ||||
| import { ModelForm } from "@goauthentik/elements/forms/ModelForm"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { TemplateResult, html } from "lit"; | ||||
| import { customElement } from "lit/decorators.js"; | ||||
| import { ifDefined } from "lit/directives/if-defined.js"; | ||||
| import { until } from "lit/directives/until.js"; | ||||
|  | ||||
| import { CryptoApi, DockerServiceConnection, OutpostsApi } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-service-connection-docker-form") | ||||
| export class ServiceConnectionDockerForm extends ModelForm<DockerServiceConnection, string> { | ||||
|     loadInstance(pk: string): Promise<DockerServiceConnection> { | ||||
|         return new OutpostsApi(DEFAULT_CONFIG).outpostsServiceConnectionsDockerRetrieve({ | ||||
|             uuid: pk, | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     getSuccessMessage(): string { | ||||
|         if (this.instance) { | ||||
|             return t`Successfully updated integration.`; | ||||
|         } else { | ||||
|             return t`Successfully created integration.`; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     send = (data: DockerServiceConnection): Promise<DockerServiceConnection> => { | ||||
|         if (this.instance) { | ||||
|             return new OutpostsApi(DEFAULT_CONFIG).outpostsServiceConnectionsDockerUpdate({ | ||||
|                 uuid: this.instance.pk || "", | ||||
|                 dockerServiceConnectionRequest: data, | ||||
|             }); | ||||
|         } else { | ||||
|             return new OutpostsApi(DEFAULT_CONFIG).outpostsServiceConnectionsDockerCreate({ | ||||
|                 dockerServiceConnectionRequest: 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="${ifDefined(this.instance?.name)}" | ||||
|                     class="pf-c-form-control" | ||||
|                     required | ||||
|                 /> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-element-horizontal name="local"> | ||||
|                 <div class="pf-c-check"> | ||||
|                     <input | ||||
|                         type="checkbox" | ||||
|                         class="pf-c-check__input" | ||||
|                         ?checked=${first(this.instance?.local, false)} | ||||
|                     /> | ||||
|                     <label class="pf-c-check__label"> ${t`Local`} </label> | ||||
|                 </div> | ||||
|                 <p class="pf-c-form__helper-text"> | ||||
|                     ${t`If enabled, use the local connection. Required Docker socket/Kubernetes Integration.`} | ||||
|                 </p> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-element-horizontal label=${t`Docker URL`} ?required=${true} name="url"> | ||||
|                 <input | ||||
|                     type="text" | ||||
|                     value="${ifDefined(this.instance?.url)}" | ||||
|                     class="pf-c-form-control" | ||||
|                     required | ||||
|                 /> | ||||
|                 <p class="pf-c-form__helper-text"> | ||||
|                     ${t`Can be in the format of 'unix://' when connecting to a local docker daemon, using 'ssh://' to connect via SSH, or 'https://:2376' when connecting to a remote system.`} | ||||
|                 </p> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-element-horizontal | ||||
|                 label=${t`TLS Verification Certificate`} | ||||
|                 name="tlsVerification" | ||||
|             > | ||||
|                 <select class="pf-c-form-control"> | ||||
|                     <option value="" ?selected=${this.instance?.tlsVerification === undefined}> | ||||
|                         --------- | ||||
|                     </option> | ||||
|                     ${until( | ||||
|                         new CryptoApi(DEFAULT_CONFIG) | ||||
|                             .cryptoCertificatekeypairsList({ | ||||
|                                 ordering: "name", | ||||
|                             }) | ||||
|                             .then((certs) => { | ||||
|                                 return certs.results.map((cert) => { | ||||
|                                     return html`<option | ||||
|                                         value=${ifDefined(cert.pk)} | ||||
|                                         ?selected=${this.instance?.tlsVerification === cert.pk} | ||||
|                                     > | ||||
|                                         ${cert.name} | ||||
|                                     </option>`; | ||||
|                                 }); | ||||
|                             }), | ||||
|                         html`<option | ||||
|                             value=${ifDefined(this.instance?.tlsVerification || undefined)} | ||||
|                             ?selected=${this.instance?.tlsVerification !== undefined} | ||||
|                         > | ||||
|                             ${t`Loading...`} | ||||
|                         </option>`, | ||||
|                     )} | ||||
|                 </select> | ||||
|                 <p class="pf-c-form__helper-text"> | ||||
|                     ${t`CA which the endpoint's Certificate is verified against. Can be left empty for no validation.`} | ||||
|                 </p> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-element-horizontal | ||||
|                 label=${t`TLS Authentication Certificate/SSH Keypair`} | ||||
|                 name="tlsAuthentication" | ||||
|             > | ||||
|                 <select class="pf-c-form-control"> | ||||
|                     <option value="" ?selected=${this.instance?.tlsAuthentication === undefined}> | ||||
|                         --------- | ||||
|                     </option> | ||||
|                     ${until( | ||||
|                         new CryptoApi(DEFAULT_CONFIG) | ||||
|                             .cryptoCertificatekeypairsList({ | ||||
|                                 ordering: "name", | ||||
|                             }) | ||||
|                             .then((certs) => { | ||||
|                                 return certs.results.map((cert) => { | ||||
|                                     return html`<option | ||||
|                                         value=${ifDefined(cert.pk)} | ||||
|                                         ?selected=${this.instance?.tlsAuthentication === cert.pk} | ||||
|                                     > | ||||
|                                         ${cert.name} | ||||
|                                     </option>`; | ||||
|                                 }); | ||||
|                             }), | ||||
|                         html`<option | ||||
|                             value=${ifDefined(this.instance?.tlsAuthentication || undefined)} | ||||
|                             ?selected=${this.instance?.tlsAuthentication !== undefined} | ||||
|                         > | ||||
|                             ${t`Loading...`} | ||||
|                         </option>`, | ||||
|                     )} | ||||
|                 </select> | ||||
|                 <p class="pf-c-form__helper-text"> | ||||
|                     ${t`Certificate/Key used for authentication. Can be left empty for no authentication.`} | ||||
|                 </p> | ||||
|                 <p class="pf-c-form__helper-text"> | ||||
|                     ${t`When connecting via SSH, this keypair is used for authentication.`} | ||||
|                 </p> | ||||
|             </ak-form-element-horizontal> | ||||
|         </form>`; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										83
									
								
								web/src/admin/outposts/ServiceConnectionKubernetesForm.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								web/src/admin/outposts/ServiceConnectionKubernetesForm.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,83 @@ | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { first } from "@goauthentik/common/utils"; | ||||
| import "@goauthentik/elements/CodeMirror"; | ||||
| import "@goauthentik/elements/forms/HorizontalFormElement"; | ||||
| import { ModelForm } from "@goauthentik/elements/forms/ModelForm"; | ||||
| import YAML from "yaml"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { TemplateResult, html } from "lit"; | ||||
| import { customElement } from "lit/decorators.js"; | ||||
| import { ifDefined } from "lit/directives/if-defined.js"; | ||||
|  | ||||
| import { KubernetesServiceConnection, OutpostsApi } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-service-connection-kubernetes-form") | ||||
| export class ServiceConnectionKubernetesForm extends ModelForm< | ||||
|     KubernetesServiceConnection, | ||||
|     string | ||||
| > { | ||||
|     loadInstance(pk: string): Promise<KubernetesServiceConnection> { | ||||
|         return new OutpostsApi(DEFAULT_CONFIG).outpostsServiceConnectionsKubernetesRetrieve({ | ||||
|             uuid: pk, | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     getSuccessMessage(): string { | ||||
|         if (this.instance) { | ||||
|             return t`Successfully updated integration.`; | ||||
|         } else { | ||||
|             return t`Successfully created integration.`; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     send = (data: KubernetesServiceConnection): Promise<KubernetesServiceConnection> => { | ||||
|         if (this.instance) { | ||||
|             return new OutpostsApi(DEFAULT_CONFIG).outpostsServiceConnectionsKubernetesUpdate({ | ||||
|                 uuid: this.instance.pk || "", | ||||
|                 kubernetesServiceConnectionRequest: data, | ||||
|             }); | ||||
|         } else { | ||||
|             return new OutpostsApi(DEFAULT_CONFIG).outpostsServiceConnectionsKubernetesCreate({ | ||||
|                 kubernetesServiceConnectionRequest: 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="${ifDefined(this.instance?.name)}" | ||||
|                     class="pf-c-form-control" | ||||
|                     required | ||||
|                 /> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-element-horizontal name="local"> | ||||
|                 <div class="pf-c-check"> | ||||
|                     <input | ||||
|                         type="checkbox" | ||||
|                         class="pf-c-check__input" | ||||
|                         ?checked=${first(this.instance?.local, false)} | ||||
|                     /> | ||||
|                     <label class="pf-c-check__label"> ${t`Local`} </label> | ||||
|                 </div> | ||||
|                 <p class="pf-c-form__helper-text"> | ||||
|                     ${t`If enabled, use the local connection. Required Docker socket/Kubernetes Integration.`} | ||||
|                 </p> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-element-horizontal label=${t`Kubeconfig`} name="kubeconfig"> | ||||
|                 <ak-codemirror | ||||
|                     mode="yaml" | ||||
|                     value="${YAML.stringify(first(this.instance?.kubeconfig, {}))}" | ||||
|                 > | ||||
|                 </ak-codemirror> | ||||
|                 <p class="pf-c-form__helper-text"> | ||||
|                     ${t`Set custom attributes using YAML or JSON.`} | ||||
|                 </p> | ||||
|             </ak-form-element-horizontal> | ||||
|         </form>`; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										129
									
								
								web/src/admin/outposts/ServiceConnectionListPage.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										129
									
								
								web/src/admin/outposts/ServiceConnectionListPage.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,129 @@ | ||||
| import "@goauthentik/admin/outposts/OutpostHealth"; | ||||
| import "@goauthentik/admin/outposts/ServiceConnectionDockerForm"; | ||||
| import "@goauthentik/admin/outposts/ServiceConnectionKubernetesForm"; | ||||
| import "@goauthentik/admin/outposts/ServiceConnectionWizard"; | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { uiConfig } from "@goauthentik/common/ui/config"; | ||||
| import { PFColor } from "@goauthentik/elements/Label"; | ||||
| import "@goauthentik/elements/buttons/SpinnerButton"; | ||||
| import "@goauthentik/elements/forms/DeleteBulkForm"; | ||||
| import "@goauthentik/elements/forms/ModalForm"; | ||||
| import "@goauthentik/elements/forms/ProxyForm"; | ||||
| import { PaginatedResponse } from "@goauthentik/elements/table/Table"; | ||||
| import { TableColumn } from "@goauthentik/elements/table/Table"; | ||||
| import { TablePage } from "@goauthentik/elements/table/TablePage"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { TemplateResult, html } from "lit"; | ||||
| import { customElement, property } from "lit/decorators.js"; | ||||
| import { ifDefined } from "lit/directives/if-defined.js"; | ||||
| import { until } from "lit/directives/until.js"; | ||||
|  | ||||
| import { OutpostsApi, ServiceConnection } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-outpost-service-connection-list") | ||||
| export class OutpostServiceConnectionListPage extends TablePage<ServiceConnection> { | ||||
|     pageTitle(): string { | ||||
|         return "Outpost integrations"; | ||||
|     } | ||||
|     pageDescription(): string | undefined { | ||||
|         return "Outpost integrations define how authentik connects to external platforms to manage and deploy Outposts."; | ||||
|     } | ||||
|     pageIcon(): string { | ||||
|         return "pf-icon pf-icon-integration"; | ||||
|     } | ||||
|     searchEnabled(): boolean { | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     checkbox = true; | ||||
|  | ||||
|     async apiEndpoint(page: number): Promise<PaginatedResponse<ServiceConnection>> { | ||||
|         return new OutpostsApi(DEFAULT_CONFIG).outpostsServiceConnectionsAllList({ | ||||
|             ordering: this.order, | ||||
|             page: page, | ||||
|             pageSize: (await uiConfig()).pagination.perPage, | ||||
|             search: this.search || "", | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     columns(): TableColumn[] { | ||||
|         return [ | ||||
|             new TableColumn(t`Name`, "name"), | ||||
|             new TableColumn(t`Type`), | ||||
|             new TableColumn(t`Local`, "local"), | ||||
|             new TableColumn(t`State`), | ||||
|             new TableColumn(t`Actions`), | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     @property() | ||||
|     order = "name"; | ||||
|  | ||||
|     row(item: ServiceConnection): TemplateResult[] { | ||||
|         return [ | ||||
|             html`${item.name}`, | ||||
|             html`${item.verboseName}`, | ||||
|             html`<ak-label color=${item.local ? PFColor.Grey : PFColor.Green}> | ||||
|                 ${item.local ? t`Yes` : t`No`} | ||||
|             </ak-label>`, | ||||
|             html`${until( | ||||
|                 new OutpostsApi(DEFAULT_CONFIG) | ||||
|                     .outpostsServiceConnectionsAllStateRetrieve({ | ||||
|                         uuid: item.pk || "", | ||||
|                     }) | ||||
|                     .then((state) => { | ||||
|                         if (state.healthy) { | ||||
|                             return html`<ak-label color=${PFColor.Green} | ||||
|                                 >${ifDefined(state.version)}</ak-label | ||||
|                             >`; | ||||
|                         } | ||||
|                         return html`<ak-label color=${PFColor.Red}>${t`Unhealthy`}</ak-label>`; | ||||
|                     }), | ||||
|                 html`<ak-spinner></ak-spinner>`, | ||||
|             )}`, | ||||
|             html` <ak-forms-modal> | ||||
|                 <span slot="submit"> ${t`Update`} </span> | ||||
|                 <span slot="header"> ${t`Update ${item.verboseName}`} </span> | ||||
|                 <ak-proxy-form | ||||
|                     slot="form" | ||||
|                     .args=${{ | ||||
|                         instancePk: item.pk, | ||||
|                     }} | ||||
|                     type=${ifDefined(item.component)} | ||||
|                 > | ||||
|                 </ak-proxy-form> | ||||
|                 <button slot="trigger" class="pf-c-button pf-m-plain"> | ||||
|                     <i class="fas fa-edit"></i> | ||||
|                 </button> | ||||
|             </ak-forms-modal>`, | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     renderToolbarSelected(): TemplateResult { | ||||
|         const disabled = this.selectedElements.length < 1; | ||||
|         return html`<ak-forms-delete-bulk | ||||
|             objectLabel=${t`Outpost integration(s)`} | ||||
|             .objects=${this.selectedElements} | ||||
|             .usedBy=${(item: ServiceConnection) => { | ||||
|                 return new OutpostsApi(DEFAULT_CONFIG).outpostsServiceConnectionsAllUsedByList({ | ||||
|                     uuid: item.pk, | ||||
|                 }); | ||||
|             }} | ||||
|             .delete=${(item: ServiceConnection) => { | ||||
|                 return new OutpostsApi(DEFAULT_CONFIG).outpostsServiceConnectionsAllDestroy({ | ||||
|                     uuid: item.pk, | ||||
|                 }); | ||||
|             }} | ||||
|         > | ||||
|             <button ?disabled=${disabled} slot="trigger" class="pf-c-button pf-m-danger"> | ||||
|                 ${t`Delete`} | ||||
|             </button> | ||||
|         </ak-forms-delete-bulk>`; | ||||
|     } | ||||
|  | ||||
|     renderObjectCreate(): TemplateResult { | ||||
|         return html`<ak-service-connection-wizard></ak-service-connection-wizard> `; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										105
									
								
								web/src/admin/outposts/ServiceConnectionWizard.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								web/src/admin/outposts/ServiceConnectionWizard.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,105 @@ | ||||
| import "@goauthentik/admin/outposts/ServiceConnectionDockerForm"; | ||||
| import "@goauthentik/admin/outposts/ServiceConnectionKubernetesForm"; | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { AKElement } from "@goauthentik/elements/Base"; | ||||
| import "@goauthentik/elements/forms/ProxyForm"; | ||||
| import "@goauthentik/elements/wizard/FormWizardPage"; | ||||
| import "@goauthentik/elements/wizard/Wizard"; | ||||
| import { WizardPage } from "@goauthentik/elements/wizard/WizardPage"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { customElement } from "@lit/reactive-element/decorators/custom-element.js"; | ||||
| import { CSSResult, TemplateResult, html } from "lit"; | ||||
| import { property } from "lit/decorators.js"; | ||||
|  | ||||
| import AKGlobal from "@goauthentik/common/styles/authentik.css"; | ||||
| import PFButton from "@patternfly/patternfly/components/Button/button.css"; | ||||
| import PFForm from "@patternfly/patternfly/components/Form/form.css"; | ||||
| import PFRadio from "@patternfly/patternfly/components/Radio/radio.css"; | ||||
| import PFBase from "@patternfly/patternfly/patternfly-base.css"; | ||||
|  | ||||
| import { OutpostsApi, TypeCreate } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-service-connection-wizard-initial") | ||||
| export class InitialServiceConnectionWizardPage extends WizardPage { | ||||
|     @property({ attribute: false }) | ||||
|     connectionTypes: TypeCreate[] = []; | ||||
|  | ||||
|     static get styles(): CSSResult[] { | ||||
|         return [PFBase, PFForm, PFButton, AKGlobal, PFRadio]; | ||||
|     } | ||||
|     sidebarLabel = () => t`Select type`; | ||||
|  | ||||
|     render(): TemplateResult { | ||||
|         return html`<form class="pf-c-form pf-m-horizontal"> | ||||
|             ${this.connectionTypes.map((type) => { | ||||
|                 return html`<div class="pf-c-radio"> | ||||
|                     <input | ||||
|                         class="pf-c-radio__input" | ||||
|                         type="radio" | ||||
|                         name="type" | ||||
|                         id=${`${type.component}-${type.modelName}`} | ||||
|                         @change=${() => { | ||||
|                             this.host.steps = [ | ||||
|                                 "initial", | ||||
|                                 `type-${type.component}-${type.modelName}`, | ||||
|                             ]; | ||||
|                             this.host.isValid = true; | ||||
|                         }} | ||||
|                     /> | ||||
|                     <label class="pf-c-radio__label" for=${`${type.component}-${type.modelName}`} | ||||
|                         >${type.name}</label | ||||
|                     > | ||||
|                     <span class="pf-c-radio__description">${type.description}</span> | ||||
|                 </div>`; | ||||
|             })} | ||||
|         </form>`; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @customElement("ak-service-connection-wizard") | ||||
| export class ServiceConnectionWizard extends AKElement { | ||||
|     static get styles(): CSSResult[] { | ||||
|         return [PFBase, PFButton, AKGlobal, PFRadio]; | ||||
|     } | ||||
|  | ||||
|     @property() | ||||
|     createText = t`Create`; | ||||
|  | ||||
|     @property({ attribute: false }) | ||||
|     connectionTypes: TypeCreate[] = []; | ||||
|  | ||||
|     firstUpdated(): void { | ||||
|         new OutpostsApi(DEFAULT_CONFIG).outpostsServiceConnectionsAllTypesList().then((types) => { | ||||
|             this.connectionTypes = types; | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     render(): TemplateResult { | ||||
|         return html` | ||||
|             <ak-wizard | ||||
|                 .steps=${["initial"]} | ||||
|                 header=${t`New outpost integration`} | ||||
|                 description=${t`Create a new outpost integration.`} | ||||
|             > | ||||
|                 <ak-service-connection-wizard-initial | ||||
|                     slot="initial" | ||||
|                     .connectionTypes=${this.connectionTypes} | ||||
|                 > | ||||
|                 </ak-service-connection-wizard-initial> | ||||
|                 ${this.connectionTypes.map((type) => { | ||||
|                     return html` | ||||
|                         <ak-wizard-page-form | ||||
|                             slot=${`type-${type.component}-${type.modelName}`} | ||||
|                             .sidebarLabel=${() => t`Create ${type.name}`} | ||||
|                         > | ||||
|                             <ak-proxy-form type=${type.component}></ak-proxy-form> | ||||
|                         </ak-wizard-page-form> | ||||
|                     `; | ||||
|                 })} | ||||
|                 <button slot="trigger" class="pf-c-button pf-m-primary">${this.createText}</button> | ||||
|             </ak-wizard> | ||||
|         `; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										204
									
								
								web/src/admin/policies/BoundPoliciesList.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										204
									
								
								web/src/admin/policies/BoundPoliciesList.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,204 @@ | ||||
| import "@goauthentik/admin/groups/GroupForm"; | ||||
| import "@goauthentik/admin/policies/PolicyBindingForm"; | ||||
| import "@goauthentik/admin/policies/PolicyWizard"; | ||||
| import "@goauthentik/admin/users/UserForm"; | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { uiConfig } from "@goauthentik/common/ui/config"; | ||||
| import { PFColor } from "@goauthentik/elements/Label"; | ||||
| import { PFSize } from "@goauthentik/elements/Spinner"; | ||||
| import "@goauthentik/elements/Tabs"; | ||||
| import "@goauthentik/elements/forms/DeleteBulkForm"; | ||||
| import "@goauthentik/elements/forms/ModalForm"; | ||||
| import "@goauthentik/elements/forms/ProxyForm"; | ||||
| import { PaginatedResponse } from "@goauthentik/elements/table/Table"; | ||||
| import { Table, TableColumn } from "@goauthentik/elements/table/Table"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { TemplateResult, html } from "lit"; | ||||
| import { customElement, property } from "lit/decorators.js"; | ||||
| import { ifDefined } from "lit/directives/if-defined.js"; | ||||
|  | ||||
| import { PoliciesApi, PolicyBinding } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-bound-policies-list") | ||||
| export class BoundPoliciesList extends Table<PolicyBinding> { | ||||
|     @property() | ||||
|     target?: string; | ||||
|  | ||||
|     @property({ type: Boolean }) | ||||
|     policyOnly = false; | ||||
|  | ||||
|     checkbox = true; | ||||
|  | ||||
|     async apiEndpoint(page: number): Promise<PaginatedResponse<PolicyBinding>> { | ||||
|         return new PoliciesApi(DEFAULT_CONFIG).policiesBindingsList({ | ||||
|             target: this.target || "", | ||||
|             ordering: "order", | ||||
|             page: page, | ||||
|             pageSize: (await uiConfig()).pagination.perPage, | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     columns(): TableColumn[] { | ||||
|         return [ | ||||
|             new TableColumn(t`Policy / User / Group`), | ||||
|             new TableColumn(t`Enabled`, "enabled"), | ||||
|             new TableColumn(t`Order`, "order"), | ||||
|             new TableColumn(t`Timeout`, "timeout"), | ||||
|             new TableColumn(t`Actions`), | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     getPolicyUserGroupRowLabel(item: PolicyBinding): string { | ||||
|         if (item.policy) { | ||||
|             return t`Policy ${item.policyObj?.name}`; | ||||
|         } else if (item.group) { | ||||
|             return t`Group ${item.groupObj?.name}`; | ||||
|         } else if (item.user) { | ||||
|             return t`User ${item.userObj?.name}`; | ||||
|         } else { | ||||
|             return t`-`; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     getPolicyUserGroupRow(item: PolicyBinding): TemplateResult { | ||||
|         const label = this.getPolicyUserGroupRowLabel(item); | ||||
|         if (item.user) { | ||||
|             return html` <a href=${`#/identity/users/${item.user}`}> ${label} </a> `; | ||||
|         } | ||||
|         if (item.group) { | ||||
|             return html` <a href=${`#/identity/groups/${item.group}`}> ${label} </a> `; | ||||
|         } | ||||
|         return html`${label}`; | ||||
|     } | ||||
|  | ||||
|     getObjectEditButton(item: PolicyBinding): TemplateResult { | ||||
|         if (item.policy) { | ||||
|             return html`<ak-forms-modal> | ||||
|                 <span slot="submit"> ${t`Update`} </span> | ||||
|                 <span slot="header"> ${t`Update ${item.policyObj?.name}`} </span> | ||||
|                 <ak-proxy-form | ||||
|                     slot="form" | ||||
|                     .args=${{ | ||||
|                         instancePk: item.policyObj?.pk, | ||||
|                     }} | ||||
|                     type=${ifDefined(item.policyObj?.component)} | ||||
|                 > | ||||
|                 </ak-proxy-form> | ||||
|                 <button slot="trigger" class="pf-c-button pf-m-secondary">${t`Edit Policy`}</button> | ||||
|             </ak-forms-modal>`; | ||||
|         } else if (item.group) { | ||||
|             return html`<ak-forms-modal> | ||||
|                 <span slot="submit"> ${t`Update`} </span> | ||||
|                 <span slot="header"> ${t`Update Group`} </span> | ||||
|                 <ak-group-form slot="form" .instancePk=${item.groupObj?.pk}> </ak-group-form> | ||||
|                 <button slot="trigger" class="pf-c-button pf-m-secondary">${t`Edit Group`}</button> | ||||
|             </ak-forms-modal>`; | ||||
|         } else if (item.user) { | ||||
|             return html`<ak-forms-modal> | ||||
|                 <span slot="submit"> ${t`Update`} </span> | ||||
|                 <span slot="header"> ${t`Update User`} </span> | ||||
|                 <ak-user-form slot="form" .instancePk=${item.userObj?.pk}> </ak-user-form> | ||||
|                 <button slot="trigger" class="pf-c-button pf-m-secondary">${t`Edit User`}</button> | ||||
|             </ak-forms-modal>`; | ||||
|         } else { | ||||
|             return html``; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     renderToolbarSelected(): TemplateResult { | ||||
|         const disabled = this.selectedElements.length < 1; | ||||
|         return html`<ak-forms-delete-bulk | ||||
|             objectLabel=${t`Policy binding(s)`} | ||||
|             .objects=${this.selectedElements} | ||||
|             .metadata=${(item: PolicyBinding) => { | ||||
|                 return [ | ||||
|                     { key: t`Order`, value: item.order.toString() }, | ||||
|                     { key: t`Policy / User / Group`, value: this.getPolicyUserGroupRowLabel(item) }, | ||||
|                 ]; | ||||
|             }} | ||||
|             .usedBy=${(item: PolicyBinding) => { | ||||
|                 return new PoliciesApi(DEFAULT_CONFIG).policiesBindingsUsedByList({ | ||||
|                     policyBindingUuid: item.pk, | ||||
|                 }); | ||||
|             }} | ||||
|             .delete=${(item: PolicyBinding) => { | ||||
|                 return new PoliciesApi(DEFAULT_CONFIG).policiesBindingsDestroy({ | ||||
|                     policyBindingUuid: item.pk, | ||||
|                 }); | ||||
|             }} | ||||
|         > | ||||
|             <button ?disabled=${disabled} slot="trigger" class="pf-c-button pf-m-danger"> | ||||
|                 ${t`Delete`} | ||||
|             </button> | ||||
|         </ak-forms-delete-bulk>`; | ||||
|     } | ||||
|  | ||||
|     row(item: PolicyBinding): TemplateResult[] { | ||||
|         return [ | ||||
|             html`${this.getPolicyUserGroupRow(item)}`, | ||||
|             html` <ak-label color=${item.enabled ? PFColor.Green : PFColor.Orange}> | ||||
|                 ${item.enabled ? t`Yes` : t`No`} | ||||
|             </ak-label>`, | ||||
|             html`${item.order}`, | ||||
|             html`${item.timeout}`, | ||||
|             html` ${this.getObjectEditButton(item)} | ||||
|                 <ak-forms-modal size=${PFSize.Medium}> | ||||
|                     <span slot="submit"> ${t`Update`} </span> | ||||
|                     <span slot="header"> ${t`Update Binding`} </span> | ||||
|                     <ak-policy-binding-form | ||||
|                         slot="form" | ||||
|                         .instancePk=${item.pk} | ||||
|                         targetPk=${ifDefined(this.target)} | ||||
|                         ?policyOnly=${this.policyOnly} | ||||
|                     > | ||||
|                     </ak-policy-binding-form> | ||||
|                     <button slot="trigger" class="pf-c-button pf-m-secondary"> | ||||
|                         ${t`Edit Binding`} | ||||
|                     </button> | ||||
|                 </ak-forms-modal>`, | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     renderEmpty(): TemplateResult { | ||||
|         return super.renderEmpty(html`<ak-empty-state | ||||
|             header=${t`No Policies bound.`} | ||||
|             icon="pf-icon-module" | ||||
|         > | ||||
|             <div slot="body">${t`No policies are currently bound to this object.`}</div> | ||||
|             <div slot="primary"> | ||||
|                 <ak-forms-modal size=${PFSize.Medium}> | ||||
|                     <span slot="submit"> ${t`Create`} </span> | ||||
|                     <span slot="header"> ${t`Create Binding`} </span> | ||||
|                     <ak-policy-binding-form | ||||
|                         slot="form" | ||||
|                         targetPk=${ifDefined(this.target)} | ||||
|                         ?policyOnly=${this.policyOnly} | ||||
|                     > | ||||
|                     </ak-policy-binding-form> | ||||
|                     <button slot="trigger" class="pf-c-button pf-m-primary"> | ||||
|                         ${t`Create Binding`} | ||||
|                     </button> | ||||
|                 </ak-forms-modal> | ||||
|             </div> | ||||
|         </ak-empty-state>`); | ||||
|     } | ||||
|  | ||||
|     renderToolbar(): TemplateResult { | ||||
|         return html`<ak-forms-modal size=${PFSize.Medium}> | ||||
|                 <span slot="submit"> ${t`Create`} </span> | ||||
|                 <span slot="header"> ${t`Create Binding`} </span> | ||||
|                 <ak-policy-binding-form | ||||
|                     slot="form" | ||||
|                     targetPk=${ifDefined(this.target)} | ||||
|                     ?policyOnly=${this.policyOnly} | ||||
|                 > | ||||
|                 </ak-policy-binding-form> | ||||
|                 <button slot="trigger" class="pf-c-button pf-m-primary"> | ||||
|                     ${t`Create Binding`} | ||||
|                 </button> | ||||
|             </ak-forms-modal> | ||||
|             <ak-policy-wizard createText=${t`Create Policy`}></ak-policy-wizard> `; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										325
									
								
								web/src/admin/policies/PolicyBindingForm.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										325
									
								
								web/src/admin/policies/PolicyBindingForm.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,325 @@ | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { first, groupBy } from "@goauthentik/common/utils"; | ||||
| import "@goauthentik/elements/SearchSelect"; | ||||
| import "@goauthentik/elements/forms/HorizontalFormElement"; | ||||
| import { ModelForm } from "@goauthentik/elements/forms/ModelForm"; | ||||
| import { UserOption } from "@goauthentik/elements/user/utils"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { CSSResult, css } from "lit"; | ||||
| import { TemplateResult, html } from "lit"; | ||||
| import { customElement, property } from "lit/decorators.js"; | ||||
| import { ifDefined } from "lit/directives/if-defined.js"; | ||||
| import { until } from "lit/directives/until.js"; | ||||
|  | ||||
| import PFContent from "@patternfly/patternfly/components/Content/content.css"; | ||||
| import PFToggleGroup from "@patternfly/patternfly/components/ToggleGroup/toggle-group.css"; | ||||
|  | ||||
| import { | ||||
|     CoreApi, | ||||
|     CoreGroupsListRequest, | ||||
|     CoreUsersListRequest, | ||||
|     Group, | ||||
|     PoliciesApi, | ||||
|     Policy, | ||||
|     PolicyBinding, | ||||
|     User, | ||||
| } from "@goauthentik/api"; | ||||
|  | ||||
| enum target { | ||||
|     policy, | ||||
|     group, | ||||
|     user, | ||||
| } | ||||
|  | ||||
| @customElement("ak-policy-binding-form") | ||||
| export class PolicyBindingForm extends ModelForm<PolicyBinding, string> { | ||||
|     loadInstance(pk: string): Promise<PolicyBinding> { | ||||
|         return new PoliciesApi(DEFAULT_CONFIG) | ||||
|             .policiesBindingsRetrieve({ | ||||
|                 policyBindingUuid: pk, | ||||
|             }) | ||||
|             .then((binding) => { | ||||
|                 if (binding?.policyObj) { | ||||
|                     this.policyGroupUser = target.policy; | ||||
|                 } | ||||
|                 if (binding?.groupObj) { | ||||
|                     this.policyGroupUser = target.group; | ||||
|                 } | ||||
|                 if (binding?.userObj) { | ||||
|                     this.policyGroupUser = target.user; | ||||
|                 } | ||||
|                 return binding; | ||||
|             }); | ||||
|     } | ||||
|  | ||||
|     @property() | ||||
|     targetPk?: string; | ||||
|  | ||||
|     @property({ type: Number }) | ||||
|     policyGroupUser: target = target.policy; | ||||
|  | ||||
|     @property({ type: Boolean }) | ||||
|     policyOnly = false; | ||||
|  | ||||
|     getSuccessMessage(): string { | ||||
|         if (this.instance) { | ||||
|             return t`Successfully updated binding.`; | ||||
|         } else { | ||||
|             return t`Successfully created binding.`; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     static get styles(): CSSResult[] { | ||||
|         return super.styles.concat( | ||||
|             PFToggleGroup, | ||||
|             PFContent, | ||||
|             css` | ||||
|                 .pf-c-toggle-group { | ||||
|                     justify-content: center; | ||||
|                 } | ||||
|             `, | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     send = (data: PolicyBinding): Promise<PolicyBinding> => { | ||||
|         if (this.instance) { | ||||
|             return new PoliciesApi(DEFAULT_CONFIG).policiesBindingsUpdate({ | ||||
|                 policyBindingUuid: this.instance.pk || "", | ||||
|                 policyBindingRequest: data, | ||||
|             }); | ||||
|         } else { | ||||
|             return new PoliciesApi(DEFAULT_CONFIG).policiesBindingsCreate({ | ||||
|                 policyBindingRequest: data, | ||||
|             }); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     groupPolicies(policies: Policy[]): TemplateResult { | ||||
|         return html` | ||||
|             ${groupBy<Policy>(policies, (p) => p.verboseName || "").map(([group, policies]) => { | ||||
|                 return html`<optgroup label=${group}> | ||||
|                     ${policies.map((p) => { | ||||
|                         const selected = this.instance?.policy === p.pk; | ||||
|                         return html`<option ?selected=${selected} value=${ifDefined(p.pk)}> | ||||
|                             ${p.name} | ||||
|                         </option>`; | ||||
|                     })} | ||||
|                 </optgroup>`; | ||||
|             })} | ||||
|         `; | ||||
|     } | ||||
|  | ||||
|     getOrder(): Promise<number> { | ||||
|         if (this.instance) { | ||||
|             return Promise.resolve(this.instance.order); | ||||
|         } | ||||
|         return new PoliciesApi(DEFAULT_CONFIG) | ||||
|             .policiesBindingsList({ | ||||
|                 target: this.targetPk || "", | ||||
|             }) | ||||
|             .then((bindings) => { | ||||
|                 const orders = bindings.results.map((binding) => binding.order); | ||||
|                 if (orders.length < 1) { | ||||
|                     return 0; | ||||
|                 } | ||||
|                 return Math.max(...orders) + 1; | ||||
|             }); | ||||
|     } | ||||
|  | ||||
|     renderModeSelector(): TemplateResult { | ||||
|         return html` <div class="pf-c-toggle-group__item"> | ||||
|                 <button | ||||
|                     class="pf-c-toggle-group__button ${this.policyGroupUser === target.policy | ||||
|                         ? "pf-m-selected" | ||||
|                         : ""}" | ||||
|                     type="button" | ||||
|                     @click=${() => { | ||||
|                         this.policyGroupUser = target.policy; | ||||
|                     }} | ||||
|                 > | ||||
|                     <span class="pf-c-toggle-group__text">${t`Policy`}</span> | ||||
|                 </button> | ||||
|             </div> | ||||
|             <div class="pf-c-divider pf-m-vertical" role="separator"></div> | ||||
|             <div class="pf-c-toggle-group__item"> | ||||
|                 <button | ||||
|                     class="pf-c-toggle-group__button ${this.policyGroupUser === target.group | ||||
|                         ? "pf-m-selected" | ||||
|                         : ""}" | ||||
|                     type="button" | ||||
|                     @click=${() => { | ||||
|                         this.policyGroupUser = target.group; | ||||
|                     }} | ||||
|                 > | ||||
|                     <span class="pf-c-toggle-group__text">${t`Group`}</span> | ||||
|                 </button> | ||||
|             </div> | ||||
|             <div class="pf-c-divider pf-m-vertical" role="separator"></div> | ||||
|             <div class="pf-c-toggle-group__item"> | ||||
|                 <button | ||||
|                     class="pf-c-toggle-group__button ${this.policyGroupUser === target.user | ||||
|                         ? "pf-m-selected" | ||||
|                         : ""}" | ||||
|                     type="button" | ||||
|                     @click=${() => { | ||||
|                         this.policyGroupUser = target.user; | ||||
|                     }} | ||||
|                 > | ||||
|                     <span class="pf-c-toggle-group__text">${t`User`}</span> | ||||
|                 </button> | ||||
|             </div>`; | ||||
|     } | ||||
|  | ||||
|     renderForm(): TemplateResult { | ||||
|         return html`<form class="pf-c-form pf-m-horizontal"> | ||||
|             <div class="pf-c-card pf-m-selectable pf-m-selected"> | ||||
|                 <div class="pf-c-card__body"> | ||||
|                     <div class="pf-c-toggle-group">${this.renderModeSelector()}</div> | ||||
|                 </div> | ||||
|                 <div class="pf-c-card__footer"> | ||||
|                     <ak-form-element-horizontal | ||||
|                         label=${t`Policy`} | ||||
|                         name="policy" | ||||
|                         ?hidden=${this.policyGroupUser !== target.policy} | ||||
|                     > | ||||
|                         <select class="pf-c-form-control"> | ||||
|                             <option value="" ?selected=${this.instance?.policy === undefined}> | ||||
|                                 --------- | ||||
|                             </option> | ||||
|                             ${until( | ||||
|                                 new PoliciesApi(DEFAULT_CONFIG) | ||||
|                                     .policiesAllList({ | ||||
|                                         ordering: "name", | ||||
|                                     }) | ||||
|                                     .then((policies) => { | ||||
|                                         return this.groupPolicies(policies.results); | ||||
|                                     }), | ||||
|                                 html`<option>${t`Loading...`}</option>`, | ||||
|                             )} | ||||
|                         </select> | ||||
|                     </ak-form-element-horizontal> | ||||
|                     <ak-form-element-horizontal | ||||
|                         label=${t`Group`} | ||||
|                         name="group" | ||||
|                         ?hidden=${this.policyGroupUser !== target.group} | ||||
|                     > | ||||
|                         <!-- @ts-ignore --> | ||||
|                         <ak-search-select | ||||
|                             .fetchObjects=${async (query?: string): Promise<Group[]> => { | ||||
|                                 const args: CoreGroupsListRequest = { | ||||
|                                     ordering: "name", | ||||
|                                 }; | ||||
|                                 if (query !== undefined) { | ||||
|                                     args.search = query; | ||||
|                                 } | ||||
|                                 const groups = await new CoreApi(DEFAULT_CONFIG).coreGroupsList( | ||||
|                                     args, | ||||
|                                 ); | ||||
|                                 return groups.results; | ||||
|                             }} | ||||
|                             .renderElement=${(group: Group): string => { | ||||
|                                 return group.name; | ||||
|                             }} | ||||
|                             .value=${(group: Group | undefined): string | undefined => { | ||||
|                                 return group ? group.pk : undefined; | ||||
|                             }} | ||||
|                             .selected=${(group: Group): boolean => { | ||||
|                                 return group.pk === this.instance?.group; | ||||
|                             }} | ||||
|                             ?blankable=${true} | ||||
|                         > | ||||
|                         </ak-search-select> | ||||
|                         ${this.policyOnly | ||||
|                             ? html`<p class="pf-c-form__helper-text"> | ||||
|                                   ${t`Group mappings can only be checked if a user is already logged in when trying to access this source.`} | ||||
|                               </p>` | ||||
|                             : html``} | ||||
|                     </ak-form-element-horizontal> | ||||
|                     <ak-form-element-horizontal | ||||
|                         label=${t`User`} | ||||
|                         name="user" | ||||
|                         ?hidden=${this.policyGroupUser !== target.user} | ||||
|                     > | ||||
|                         <!-- @ts-ignore --> | ||||
|                         <ak-search-select | ||||
|                             .fetchObjects=${async (query?: string): Promise<User[]> => { | ||||
|                                 const args: CoreUsersListRequest = { | ||||
|                                     ordering: "username", | ||||
|                                 }; | ||||
|                                 if (query !== undefined) { | ||||
|                                     args.search = query; | ||||
|                                 } | ||||
|                                 const users = await new CoreApi(DEFAULT_CONFIG).coreUsersList(args); | ||||
|                                 return users.results; | ||||
|                             }} | ||||
|                             .renderElement=${(user: User): string => { | ||||
|                                 return UserOption(user); | ||||
|                             }} | ||||
|                             .value=${(user: User | undefined): number | undefined => { | ||||
|                                 return user ? user.pk : undefined; | ||||
|                             }} | ||||
|                             .selected=${(user: User): boolean => { | ||||
|                                 return user.pk === this.instance?.user; | ||||
|                             }} | ||||
|                             ?blankable=${true} | ||||
|                         > | ||||
|                         </ak-search-select> | ||||
|                         ${this.policyOnly | ||||
|                             ? html`<p class="pf-c-form__helper-text"> | ||||
|                                   ${t`User mappings can only be checked if a user is already logged in when trying to access this source.`} | ||||
|                               </p>` | ||||
|                             : html``} | ||||
|                     </ak-form-element-horizontal> | ||||
|                 </div> | ||||
|             </div> | ||||
|             <input | ||||
|                 required | ||||
|                 name="target" | ||||
|                 type="hidden" | ||||
|                 value=${ifDefined(this.instance?.target || this.targetPk)} | ||||
|             /> | ||||
|             <ak-form-element-horizontal name="enabled"> | ||||
|                 <div class="pf-c-check"> | ||||
|                     <input | ||||
|                         type="checkbox" | ||||
|                         class="pf-c-check__input" | ||||
|                         ?checked=${first(this.instance?.enabled, true)} | ||||
|                     /> | ||||
|                     <label class="pf-c-check__label"> ${t`Enabled`} </label> | ||||
|                 </div> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-element-horizontal name="negate"> | ||||
|                 <div class="pf-c-check"> | ||||
|                     <input | ||||
|                         type="checkbox" | ||||
|                         class="pf-c-check__input" | ||||
|                         ?checked=${first(this.instance?.negate, false)} | ||||
|                     /> | ||||
|                     <label class="pf-c-check__label"> ${t`Negate result`} </label> | ||||
|                 </div> | ||||
|                 <p class="pf-c-form__helper-text"> | ||||
|                     ${t`Negates the outcome of the binding. Messages are unaffected.`} | ||||
|                 </p> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-element-horizontal label=${t`Order`} ?required=${true} name="order"> | ||||
|                 <!-- @ts-ignore --> | ||||
|                 <input | ||||
|                     type="number" | ||||
|                     value="${until(this.getOrder())}" | ||||
|                     class="pf-c-form-control" | ||||
|                     required | ||||
|                 /> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-element-horizontal label=${t`Timeout`} ?required=${true} name="timeout"> | ||||
|                 <input | ||||
|                     type="number" | ||||
|                     value="${first(this.instance?.timeout, 30)}" | ||||
|                     class="pf-c-form-control" | ||||
|                     required | ||||
|                 /> | ||||
|             </ak-form-element-horizontal> | ||||
|         </form>`; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										154
									
								
								web/src/admin/policies/PolicyListPage.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										154
									
								
								web/src/admin/policies/PolicyListPage.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,154 @@ | ||||
| import "@goauthentik/admin/policies/PolicyTestForm"; | ||||
| import "@goauthentik/admin/policies/PolicyWizard"; | ||||
| import "@goauthentik/admin/policies/dummy/DummyPolicyForm"; | ||||
| import "@goauthentik/admin/policies/event_matcher/EventMatcherPolicyForm"; | ||||
| import "@goauthentik/admin/policies/expiry/ExpiryPolicyForm"; | ||||
| import "@goauthentik/admin/policies/expression/ExpressionPolicyForm"; | ||||
| import "@goauthentik/admin/policies/hibp/HaveIBeenPwnedPolicyForm"; | ||||
| import "@goauthentik/admin/policies/password/PasswordPolicyForm"; | ||||
| import "@goauthentik/admin/policies/reputation/ReputationPolicyForm"; | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { uiConfig } from "@goauthentik/common/ui/config"; | ||||
| import { groupBy } from "@goauthentik/common/utils"; | ||||
| import "@goauthentik/elements/forms/ConfirmationForm"; | ||||
| import "@goauthentik/elements/forms/DeleteBulkForm"; | ||||
| import "@goauthentik/elements/forms/ModalForm"; | ||||
| import "@goauthentik/elements/forms/ProxyForm"; | ||||
| import { PaginatedResponse } from "@goauthentik/elements/table/Table"; | ||||
| import { TableColumn } from "@goauthentik/elements/table/Table"; | ||||
| import { TablePage } from "@goauthentik/elements/table/TablePage"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { TemplateResult, html } from "lit"; | ||||
| import { customElement, property } from "lit/decorators.js"; | ||||
| import { ifDefined } from "lit/directives/if-defined.js"; | ||||
|  | ||||
| import { PoliciesApi, Policy } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-policy-list") | ||||
| export class PolicyListPage extends TablePage<Policy> { | ||||
|     searchEnabled(): boolean { | ||||
|         return true; | ||||
|     } | ||||
|     pageTitle(): string { | ||||
|         return t`Policies`; | ||||
|     } | ||||
|     pageDescription(): string { | ||||
|         return t`Allow users to use Applications based on properties, enforce Password Criteria and selectively apply Stages.`; | ||||
|     } | ||||
|     pageIcon(): string { | ||||
|         return "pf-icon pf-icon-infrastructure"; | ||||
|     } | ||||
|  | ||||
|     checkbox = true; | ||||
|  | ||||
|     @property() | ||||
|     order = "name"; | ||||
|  | ||||
|     async apiEndpoint(page: number): Promise<PaginatedResponse<Policy>> { | ||||
|         return new PoliciesApi(DEFAULT_CONFIG).policiesAllList({ | ||||
|             ordering: this.order, | ||||
|             page: page, | ||||
|             pageSize: (await uiConfig()).pagination.perPage, | ||||
|             search: this.search || "", | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     columns(): TableColumn[] { | ||||
|         return [ | ||||
|             new TableColumn(t`Name`, "name"), | ||||
|             new TableColumn(t`Type`), | ||||
|             new TableColumn(t`Actions`), | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     groupBy(items: Policy[]): [string, Policy[]][] { | ||||
|         return groupBy(items, (policy) => policy.verboseNamePlural); | ||||
|     } | ||||
|  | ||||
|     row(item: Policy): TemplateResult[] { | ||||
|         return [ | ||||
|             html`<div> | ||||
|                 <div>${item.name}</div> | ||||
|                 ${(item.boundTo || 0) > 0 | ||||
|                     ? html`<i class="pf-icon pf-icon-ok"></i> | ||||
|                           <small>${t`Assigned to ${item.boundTo} object(s).`}</small>` | ||||
|                     : html`<i class="pf-icon pf-icon-warning-triangle"></i> | ||||
|                           <small>${t`Warning: Policy is not assigned.`}</small>`} | ||||
|             </div>`, | ||||
|             html`${item.verboseName}`, | ||||
|             html` <ak-forms-modal> | ||||
|                     <span slot="submit"> ${t`Update`} </span> | ||||
|                     <span slot="header"> ${t`Update ${item.verboseName}`} </span> | ||||
|                     <ak-proxy-form | ||||
|                         slot="form" | ||||
|                         .args=${{ | ||||
|                             instancePk: item.pk, | ||||
|                         }} | ||||
|                         type=${ifDefined(item.component)} | ||||
|                     > | ||||
|                     </ak-proxy-form> | ||||
|                     <button slot="trigger" class="pf-c-button pf-m-plain"> | ||||
|                         <i class="fas fa-pencil-alt" aria-hidden="true"></i> | ||||
|                     </button> | ||||
|                 </ak-forms-modal> | ||||
|                 <ak-forms-modal .closeAfterSuccessfulSubmit=${false}> | ||||
|                     <span slot="submit"> ${t`Test`} </span> | ||||
|                     <span slot="header"> ${t`Test Policy`} </span> | ||||
|                     <ak-policy-test-form slot="form" .policy=${item}> </ak-policy-test-form> | ||||
|                     <button slot="trigger" class="pf-c-button pf-m-plain"> | ||||
|                         <i class="fas fa-vial" aria-hidden="true"></i> | ||||
|                     </button> | ||||
|                 </ak-forms-modal>`, | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     renderToolbarSelected(): TemplateResult { | ||||
|         const disabled = this.selectedElements.length < 1; | ||||
|         return html`<ak-forms-delete-bulk | ||||
|             objectLabel=${t`Policy / Policies`} | ||||
|             .objects=${this.selectedElements} | ||||
|             .usedBy=${(item: Policy) => { | ||||
|                 return new PoliciesApi(DEFAULT_CONFIG).policiesAllUsedByList({ | ||||
|                     policyUuid: item.pk, | ||||
|                 }); | ||||
|             }} | ||||
|             .delete=${(item: Policy) => { | ||||
|                 return new PoliciesApi(DEFAULT_CONFIG).policiesAllDestroy({ | ||||
|                     policyUuid: item.pk, | ||||
|                 }); | ||||
|             }} | ||||
|         > | ||||
|             <button ?disabled=${disabled} slot="trigger" class="pf-c-button pf-m-danger"> | ||||
|                 ${t`Delete`} | ||||
|             </button> | ||||
|         </ak-forms-delete-bulk>`; | ||||
|     } | ||||
|  | ||||
|     renderObjectCreate(): TemplateResult { | ||||
|         return html`<ak-policy-wizard> </ak-policy-wizard>`; | ||||
|     } | ||||
|  | ||||
|     renderToolbar(): TemplateResult { | ||||
|         return html` ${super.renderToolbar()} | ||||
|             <ak-forms-confirm | ||||
|                 successMessage=${t`Successfully cleared policy cache`} | ||||
|                 errorMessage=${t`Failed to delete policy cache`} | ||||
|                 action=${t`Clear cache`} | ||||
|                 .onConfirm=${() => { | ||||
|                     return new PoliciesApi(DEFAULT_CONFIG).policiesAllCacheClearCreate(); | ||||
|                 }} | ||||
|             > | ||||
|                 <span slot="header"> ${t`Clear Policy cache`} </span> | ||||
|                 <p slot="body"> | ||||
|                     ${t`Are you sure you want to clear the policy cache? | ||||
|                 This will cause all policies to be re-evaluated on their next usage.`} | ||||
|                 </p> | ||||
|                 <button slot="trigger" class="pf-c-button pf-m-secondary" type="button"> | ||||
|                     ${t`Clear cache`} | ||||
|                 </button> | ||||
|                 <div slot="modal"></div> | ||||
|             </ak-forms-confirm>`; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										154
									
								
								web/src/admin/policies/PolicyTestForm.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										154
									
								
								web/src/admin/policies/PolicyTestForm.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,154 @@ | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { first } from "@goauthentik/common/utils"; | ||||
| import "@goauthentik/elements/CodeMirror"; | ||||
| import { PFColor } from "@goauthentik/elements/Label"; | ||||
| import { Form } from "@goauthentik/elements/forms/Form"; | ||||
| import "@goauthentik/elements/forms/HorizontalFormElement"; | ||||
| import { UserOption } from "@goauthentik/elements/user/utils"; | ||||
| import YAML from "yaml"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { CSSResult, TemplateResult, html } from "lit"; | ||||
| import { customElement, property } from "lit/decorators.js"; | ||||
| import { until } from "lit/directives/until.js"; | ||||
|  | ||||
| import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css"; | ||||
|  | ||||
| import { | ||||
|     CoreApi, | ||||
|     PoliciesApi, | ||||
|     Policy, | ||||
|     PolicyTestRequest, | ||||
|     PolicyTestResult, | ||||
| } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-policy-test-form") | ||||
| export class PolicyTestForm extends Form<PolicyTestRequest> { | ||||
|     @property({ attribute: false }) | ||||
|     policy?: Policy; | ||||
|  | ||||
|     @property({ attribute: false }) | ||||
|     result?: PolicyTestResult; | ||||
|  | ||||
|     @property({ attribute: false }) | ||||
|     request?: PolicyTestRequest; | ||||
|  | ||||
|     getSuccessMessage(): string { | ||||
|         return t`Successfully sent test-request.`; | ||||
|     } | ||||
|  | ||||
|     send = (data: PolicyTestRequest): Promise<PolicyTestResult> => { | ||||
|         this.request = data; | ||||
|         return new PoliciesApi(DEFAULT_CONFIG) | ||||
|             .policiesAllTestCreate({ | ||||
|                 policyUuid: this.policy?.pk || "", | ||||
|                 policyTestRequest: data, | ||||
|             }) | ||||
|             .then((result) => (this.result = result)); | ||||
|     }; | ||||
|  | ||||
|     static get styles(): CSSResult[] { | ||||
|         return super.styles.concat(PFDescriptionList); | ||||
|     } | ||||
|  | ||||
|     renderResult(): TemplateResult { | ||||
|         return html` | ||||
|             <ak-form-element-horizontal label=${t`Passing`}> | ||||
|                 <div class="pf-c-form__group-label"> | ||||
|                     <div class="c-form__horizontal-group"> | ||||
|                         <span class="pf-c-form__label-text"> | ||||
|                             <ak-label color=${this.result?.passing ? PFColor.Green : PFColor.Red}> | ||||
|                                 ${this.result?.passing ? t`Yes` : t`No`} | ||||
|                             </ak-label> | ||||
|                         </span> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-element-horizontal label=${t`Messages`}> | ||||
|                 <div class="pf-c-form__group-label"> | ||||
|                     <div class="c-form__horizontal-group"> | ||||
|                         <ul> | ||||
|                             ${(this.result?.messages || []).length > 0 | ||||
|                                 ? this.result?.messages?.map((m) => { | ||||
|                                       return html`<li> | ||||
|                                           <span class="pf-c-form__label-text">${m}</span> | ||||
|                                       </li>`; | ||||
|                                   }) | ||||
|                                 : html`<li> | ||||
|                                       <span class="pf-c-form__label-text">-</span> | ||||
|                                   </li>`} | ||||
|                         </ul> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </ak-form-element-horizontal> | ||||
|  | ||||
|             <ak-form-element-horizontal label=${t`Log messages`}> | ||||
|                 <div class="pf-c-form__group-label"> | ||||
|                     <div class="c-form__horizontal-group"> | ||||
|                         <dl class="pf-c-description-list pf-m-horizontal"> | ||||
|                             ${(this.result?.logMessages || []).length > 0 | ||||
|                                 ? this.result?.logMessages?.map((m) => { | ||||
|                                       return html`<div class="pf-c-description-list__group"> | ||||
|                                           <dt class="pf-c-description-list__term"> | ||||
|                                               <span class="pf-c-description-list__text" | ||||
|                                                   >${m.log_level}</span | ||||
|                                               > | ||||
|                                           </dt> | ||||
|                                           <dd class="pf-c-description-list__description"> | ||||
|                                               <div class="pf-c-description-list__text"> | ||||
|                                                   ${m.event} | ||||
|                                               </div> | ||||
|                                           </dd> | ||||
|                                       </div>`; | ||||
|                                   }) | ||||
|                                 : html`<div class="pf-c-description-list__group"> | ||||
|                                       <dt class="pf-c-description-list__term"> | ||||
|                                           <span class="pf-c-description-list__text" | ||||
|                                               >${t`No log messages.`}</span | ||||
|                                           > | ||||
|                                       </dt> | ||||
|                                   </div>`} | ||||
|                         </dl> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </ak-form-element-horizontal> | ||||
|         `; | ||||
|     } | ||||
|  | ||||
|     renderForm(): TemplateResult { | ||||
|         return html`<form class="pf-c-form pf-m-horizontal"> | ||||
|             <ak-form-element-horizontal label=${t`User`} ?required=${true} name="user"> | ||||
|                 <select class="pf-c-form-control"> | ||||
|                     ${until( | ||||
|                         new CoreApi(DEFAULT_CONFIG) | ||||
|                             .coreUsersList({ | ||||
|                                 ordering: "username", | ||||
|                             }) | ||||
|                             .then((users) => { | ||||
|                                 return users.results.map((user) => { | ||||
|                                     return html`<option | ||||
|                                         ?selected=${this.request?.user.toString() === | ||||
|                                         user.pk.toString()} | ||||
|                                         value=${user.pk} | ||||
|                                     > | ||||
|                                         ${UserOption(user)} | ||||
|                                     </option>`; | ||||
|                                 }); | ||||
|                             }), | ||||
|                         html`<option>${t`Loading...`}</option>`, | ||||
|                     )} | ||||
|                 </select> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-element-horizontal label=${t`Context`} name="context"> | ||||
|                 <ak-codemirror mode="yaml" value=${YAML.stringify(first(this.request?.context, {}))} | ||||
|                     >> | ||||
|                 </ak-codemirror> | ||||
|                 <p class="pf-c-form__helper-text"> | ||||
|                     ${t`Set custom attributes using YAML or JSON.`} | ||||
|                 </p> | ||||
|             </ak-form-element-horizontal> | ||||
|             ${this.result ? this.renderResult() : html``} | ||||
|         </form>`; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										107
									
								
								web/src/admin/policies/PolicyWizard.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								web/src/admin/policies/PolicyWizard.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,107 @@ | ||||
| import "@goauthentik/admin/policies/dummy/DummyPolicyForm"; | ||||
| import "@goauthentik/admin/policies/event_matcher/EventMatcherPolicyForm"; | ||||
| import "@goauthentik/admin/policies/expiry/ExpiryPolicyForm"; | ||||
| import "@goauthentik/admin/policies/expression/ExpressionPolicyForm"; | ||||
| import "@goauthentik/admin/policies/hibp/HaveIBeenPwnedPolicyForm"; | ||||
| import "@goauthentik/admin/policies/password/PasswordPolicyForm"; | ||||
| import "@goauthentik/admin/policies/reputation/ReputationPolicyForm"; | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { AKElement } from "@goauthentik/elements/Base"; | ||||
| import "@goauthentik/elements/forms/ProxyForm"; | ||||
| import "@goauthentik/elements/wizard/FormWizardPage"; | ||||
| import "@goauthentik/elements/wizard/Wizard"; | ||||
| import { WizardPage } from "@goauthentik/elements/wizard/WizardPage"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { customElement } from "@lit/reactive-element/decorators/custom-element.js"; | ||||
| import { CSSResult, TemplateResult, html } from "lit"; | ||||
| import { property } from "lit/decorators.js"; | ||||
|  | ||||
| import AKGlobal from "@goauthentik/common/styles/authentik.css"; | ||||
| import PFButton from "@patternfly/patternfly/components/Button/button.css"; | ||||
| import PFForm from "@patternfly/patternfly/components/Form/form.css"; | ||||
| import PFRadio from "@patternfly/patternfly/components/Radio/radio.css"; | ||||
| import PFBase from "@patternfly/patternfly/patternfly-base.css"; | ||||
|  | ||||
| import { PoliciesApi, TypeCreate } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-policy-wizard-initial") | ||||
| export class InitialPolicyWizardPage extends WizardPage { | ||||
|     @property({ attribute: false }) | ||||
|     policyTypes: TypeCreate[] = []; | ||||
|  | ||||
|     static get styles(): CSSResult[] { | ||||
|         return [PFBase, PFForm, PFButton, AKGlobal, PFRadio]; | ||||
|     } | ||||
|     sidebarLabel = () => t`Select type`; | ||||
|  | ||||
|     render(): TemplateResult { | ||||
|         return html`<form class="pf-c-form pf-m-horizontal"> | ||||
|             ${this.policyTypes.map((type) => { | ||||
|                 return html`<div class="pf-c-radio"> | ||||
|                     <input | ||||
|                         class="pf-c-radio__input" | ||||
|                         type="radio" | ||||
|                         name="type" | ||||
|                         id=${`${type.component}-${type.modelName}`} | ||||
|                         @change=${() => { | ||||
|                             this.host.steps = [ | ||||
|                                 "initial", | ||||
|                                 `type-${type.component}-${type.modelName}`, | ||||
|                             ]; | ||||
|                             this.host.isValid = true; | ||||
|                         }} | ||||
|                     /> | ||||
|                     <label class="pf-c-radio__label" for=${`${type.component}-${type.modelName}`} | ||||
|                         >${type.name}</label | ||||
|                     > | ||||
|                     <span class="pf-c-radio__description">${type.description}</span> | ||||
|                 </div>`; | ||||
|             })} | ||||
|         </form>`; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @customElement("ak-policy-wizard") | ||||
| export class PolicyWizard extends AKElement { | ||||
|     static get styles(): CSSResult[] { | ||||
|         return [PFBase, PFButton, AKGlobal, PFRadio]; | ||||
|     } | ||||
|  | ||||
|     @property() | ||||
|     createText = t`Create`; | ||||
|  | ||||
|     @property({ attribute: false }) | ||||
|     policyTypes: TypeCreate[] = []; | ||||
|  | ||||
|     firstUpdated(): void { | ||||
|         new PoliciesApi(DEFAULT_CONFIG).policiesAllTypesList().then((types) => { | ||||
|             this.policyTypes = types; | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     render(): TemplateResult { | ||||
|         return html` | ||||
|             <ak-wizard | ||||
|                 .steps=${["initial"]} | ||||
|                 header=${t`New policy`} | ||||
|                 description=${t`Create a new policy.`} | ||||
|             > | ||||
|                 <ak-policy-wizard-initial slot="initial" .policyTypes=${this.policyTypes}> | ||||
|                 </ak-policy-wizard-initial> | ||||
|                 ${this.policyTypes.map((type) => { | ||||
|                     return html` | ||||
|                         <ak-wizard-page-form | ||||
|                             slot=${`type-${type.component}-${type.modelName}`} | ||||
|                             .sidebarLabel=${() => t`Create ${type.name}`} | ||||
|                         > | ||||
|                             <ak-proxy-form type=${type.component}></ak-proxy-form> | ||||
|                         </ak-wizard-page-form> | ||||
|                     `; | ||||
|                 })} | ||||
|                 <button slot="trigger" class="pf-c-button pf-m-primary">${this.createText}</button> | ||||
|             </ak-wizard> | ||||
|         `; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										114
									
								
								web/src/admin/policies/dummy/DummyPolicyForm.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										114
									
								
								web/src/admin/policies/dummy/DummyPolicyForm.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,114 @@ | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { first } from "@goauthentik/common/utils"; | ||||
| import "@goauthentik/elements/forms/FormGroup"; | ||||
| import "@goauthentik/elements/forms/HorizontalFormElement"; | ||||
| import { ModelForm } from "@goauthentik/elements/forms/ModelForm"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { TemplateResult, html } from "lit"; | ||||
| import { customElement } from "lit/decorators.js"; | ||||
| import { ifDefined } from "lit/directives/if-defined.js"; | ||||
|  | ||||
| import { DummyPolicy, PoliciesApi } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-policy-dummy-form") | ||||
| export class DummyPolicyForm extends ModelForm<DummyPolicy, string> { | ||||
|     loadInstance(pk: string): Promise<DummyPolicy> { | ||||
|         return new PoliciesApi(DEFAULT_CONFIG).policiesDummyRetrieve({ | ||||
|             policyUuid: pk, | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     getSuccessMessage(): string { | ||||
|         if (this.instance) { | ||||
|             return t`Successfully updated policy.`; | ||||
|         } else { | ||||
|             return t`Successfully created policy.`; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     send = (data: DummyPolicy): Promise<DummyPolicy> => { | ||||
|         if (this.instance) { | ||||
|             return new PoliciesApi(DEFAULT_CONFIG).policiesDummyUpdate({ | ||||
|                 policyUuid: this.instance.pk || "", | ||||
|                 dummyPolicyRequest: data, | ||||
|             }); | ||||
|         } else { | ||||
|             return new PoliciesApi(DEFAULT_CONFIG).policiesDummyCreate({ | ||||
|                 dummyPolicyRequest: data, | ||||
|             }); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     renderForm(): TemplateResult { | ||||
|         return html`<form class="pf-c-form pf-m-horizontal"> | ||||
|             <div class="form-help-text"> | ||||
|                 ${t`A policy used for testing. Always returns the same result as specified below after waiting a random duration.`} | ||||
|             </div> | ||||
|             <ak-form-element-horizontal label=${t`Name`} ?required=${true} name="name"> | ||||
|                 <input | ||||
|                     type="text" | ||||
|                     value="${ifDefined(this.instance?.name || "")}" | ||||
|                     class="pf-c-form-control" | ||||
|                     required | ||||
|                 /> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-element-horizontal name="executionLogging"> | ||||
|                 <div class="pf-c-check"> | ||||
|                     <input | ||||
|                         type="checkbox" | ||||
|                         class="pf-c-check__input" | ||||
|                         ?checked=${first(this.instance?.executionLogging, false)} | ||||
|                     /> | ||||
|                     <label class="pf-c-check__label"> ${t`Execution logging`} </label> | ||||
|                 </div> | ||||
|                 <p class="pf-c-form__helper-text"> | ||||
|                     ${t`When this option is enabled, all executions of this policy will be logged. By default, only execution errors are logged.`} | ||||
|                 </p> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-group .expanded=${true}> | ||||
|                 <span slot="header"> ${t`Policy-specific settings`} </span> | ||||
|                 <div slot="body" class="pf-c-form"> | ||||
|                     <ak-form-element-horizontal name="result"> | ||||
|                         <div class="pf-c-check"> | ||||
|                             <input | ||||
|                                 type="checkbox" | ||||
|                                 class="pf-c-check__input" | ||||
|                                 ?checked=${first(this.instance?.result, false)} | ||||
|                             /> | ||||
|                             <label class="pf-c-check__label"> ${t`Pass policy?`} </label> | ||||
|                         </div> | ||||
|                     </ak-form-element-horizontal> | ||||
|                     <ak-form-element-horizontal | ||||
|                         label=${t`Wait (min)`} | ||||
|                         ?required=${true} | ||||
|                         name="waitMin" | ||||
|                     > | ||||
|                         <input | ||||
|                             type="number" | ||||
|                             value="${first(this.instance?.waitMin, 1)}" | ||||
|                             class="pf-c-form-control" | ||||
|                             required | ||||
|                         /> | ||||
|                         <p class="pf-c-form__helper-text"> | ||||
|                             ${t`The policy takes a random time to execute. This controls the minimum time it will take.`} | ||||
|                         </p> | ||||
|                     </ak-form-element-horizontal> | ||||
|                     <ak-form-element-horizontal | ||||
|                         label=${t`Wait (max)`} | ||||
|                         ?required=${true} | ||||
|                         name="waitMax" | ||||
|                     > | ||||
|                         <input | ||||
|                             type="number" | ||||
|                             value="${first(this.instance?.waitMax, 5)}" | ||||
|                             class="pf-c-form-control" | ||||
|                             required | ||||
|                         /> | ||||
|                     </ak-form-element-horizontal> | ||||
|                 </div> | ||||
|             </ak-form-group> | ||||
|         </form>`; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										137
									
								
								web/src/admin/policies/event_matcher/EventMatcherPolicyForm.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										137
									
								
								web/src/admin/policies/event_matcher/EventMatcherPolicyForm.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,137 @@ | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { first } from "@goauthentik/common/utils"; | ||||
| import "@goauthentik/elements/forms/FormGroup"; | ||||
| import "@goauthentik/elements/forms/HorizontalFormElement"; | ||||
| import { ModelForm } from "@goauthentik/elements/forms/ModelForm"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { TemplateResult, html } from "lit"; | ||||
| import { customElement } from "lit/decorators.js"; | ||||
| import { ifDefined } from "lit/directives/if-defined.js"; | ||||
| import { until } from "lit/directives/until.js"; | ||||
|  | ||||
| import { AdminApi, EventMatcherPolicy, EventsApi, PoliciesApi } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-policy-event-matcher-form") | ||||
| export class EventMatcherPolicyForm extends ModelForm<EventMatcherPolicy, string> { | ||||
|     loadInstance(pk: string): Promise<EventMatcherPolicy> { | ||||
|         return new PoliciesApi(DEFAULT_CONFIG).policiesEventMatcherRetrieve({ | ||||
|             policyUuid: pk, | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     getSuccessMessage(): string { | ||||
|         if (this.instance) { | ||||
|             return t`Successfully updated policy.`; | ||||
|         } else { | ||||
|             return t`Successfully created policy.`; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     send = (data: EventMatcherPolicy): Promise<EventMatcherPolicy> => { | ||||
|         if (this.instance) { | ||||
|             return new PoliciesApi(DEFAULT_CONFIG).policiesEventMatcherUpdate({ | ||||
|                 policyUuid: this.instance.pk || "", | ||||
|                 eventMatcherPolicyRequest: data, | ||||
|             }); | ||||
|         } else { | ||||
|             return new PoliciesApi(DEFAULT_CONFIG).policiesEventMatcherCreate({ | ||||
|                 eventMatcherPolicyRequest: data, | ||||
|             }); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     renderForm(): TemplateResult { | ||||
|         return html`<form class="pf-c-form pf-m-horizontal"> | ||||
|             <div class="form-help-text"> | ||||
|                 ${t`Matches an event against a set of criteria. If any of the configured values match, the policy passes.`} | ||||
|             </div> | ||||
|             <ak-form-element-horizontal label=${t`Name`} ?required=${true} name="name"> | ||||
|                 <input | ||||
|                     type="text" | ||||
|                     value="${ifDefined(this.instance?.name || "")}" | ||||
|                     class="pf-c-form-control" | ||||
|                     required | ||||
|                 /> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-element-horizontal name="executionLogging"> | ||||
|                 <div class="pf-c-check"> | ||||
|                     <input | ||||
|                         type="checkbox" | ||||
|                         class="pf-c-check__input" | ||||
|                         ?checked=${first(this.instance?.executionLogging, false)} | ||||
|                     /> | ||||
|                     <label class="pf-c-check__label"> ${t`Execution logging`} </label> | ||||
|                 </div> | ||||
|                 <p class="pf-c-form__helper-text"> | ||||
|                     ${t`When this option is enabled, all executions of this policy will be logged. By default, only execution errors are logged.`} | ||||
|                 </p> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-group .expanded=${true}> | ||||
|                 <span slot="header"> ${t`Policy-specific settings`} </span> | ||||
|                 <div slot="body" class="pf-c-form"> | ||||
|                     <ak-form-element-horizontal label=${t`Action`} name="action"> | ||||
|                         <select class="pf-c-form-control"> | ||||
|                             <option value="" ?selected=${this.instance?.action === undefined}> | ||||
|                                 --------- | ||||
|                             </option> | ||||
|                             ${until( | ||||
|                                 new EventsApi(DEFAULT_CONFIG) | ||||
|                                     .eventsEventsActionsList() | ||||
|                                     .then((actions) => { | ||||
|                                         return actions.map((action) => { | ||||
|                                             return html`<option | ||||
|                                                 value=${action.component} | ||||
|                                                 ?selected=${this.instance?.action === | ||||
|                                                 action.component} | ||||
|                                             > | ||||
|                                                 ${action.name} | ||||
|                                             </option>`; | ||||
|                                         }); | ||||
|                                     }), | ||||
|                                 html`<option>${t`Loading...`}</option>`, | ||||
|                             )} | ||||
|                         </select> | ||||
|                         <p class="pf-c-form__helper-text"> | ||||
|                             ${t`Match created events with this action type. When left empty, all action types will be matched.`} | ||||
|                         </p> | ||||
|                     </ak-form-element-horizontal> | ||||
|                     <ak-form-element-horizontal label=${t`Client IP`} name="clientIp"> | ||||
|                         <input | ||||
|                             type="text" | ||||
|                             value="${ifDefined(this.instance?.clientIp || "")}" | ||||
|                             class="pf-c-form-control" | ||||
|                         /> | ||||
|                         <p class="pf-c-form__helper-text"> | ||||
|                             ${t`Matches Event's Client IP (strict matching, for network matching use an Expression Policy.`} | ||||
|                         </p> | ||||
|                     </ak-form-element-horizontal> | ||||
|                     <ak-form-element-horizontal label=${t`App`} name="app"> | ||||
|                         <select class="pf-c-form-control"> | ||||
|                             <option value="" ?selected=${this.instance?.app === undefined}> | ||||
|                                 --------- | ||||
|                             </option> | ||||
|                             ${until( | ||||
|                                 new AdminApi(DEFAULT_CONFIG).adminAppsList().then((apps) => { | ||||
|                                     return apps.map((app) => { | ||||
|                                         return html`<option | ||||
|                                             value=${app.name} | ||||
|                                             ?selected=${this.instance?.app === app.name} | ||||
|                                         > | ||||
|                                             ${app.label} | ||||
|                                         </option>`; | ||||
|                                     }); | ||||
|                                 }), | ||||
|                                 html`<option>${t`Loading...`}</option>`, | ||||
|                             )} | ||||
|                         </select> | ||||
|                         <p class="pf-c-form__helper-text"> | ||||
|                             ${t`Match events created by selected application. When left empty, all applications are matched.`} | ||||
|                         </p> | ||||
|                     </ak-form-element-horizontal> | ||||
|                 </div> | ||||
|             </ak-form-group> | ||||
|         </form>`; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										101
									
								
								web/src/admin/policies/expiry/ExpiryPolicyForm.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										101
									
								
								web/src/admin/policies/expiry/ExpiryPolicyForm.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,101 @@ | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { first } from "@goauthentik/common/utils"; | ||||
| import "@goauthentik/elements/forms/FormGroup"; | ||||
| import "@goauthentik/elements/forms/HorizontalFormElement"; | ||||
| import { ModelForm } from "@goauthentik/elements/forms/ModelForm"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { TemplateResult, html } from "lit"; | ||||
| import { customElement } from "lit/decorators.js"; | ||||
| import { ifDefined } from "lit/directives/if-defined.js"; | ||||
|  | ||||
| import { PasswordExpiryPolicy, PoliciesApi } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-policy-password-expiry-form") | ||||
| export class PasswordExpiryPolicyForm extends ModelForm<PasswordExpiryPolicy, string> { | ||||
|     loadInstance(pk: string): Promise<PasswordExpiryPolicy> { | ||||
|         return new PoliciesApi(DEFAULT_CONFIG).policiesPasswordExpiryRetrieve({ | ||||
|             policyUuid: pk, | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     getSuccessMessage(): string { | ||||
|         if (this.instance) { | ||||
|             return t`Successfully updated policy.`; | ||||
|         } else { | ||||
|             return t`Successfully created policy.`; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     send = (data: PasswordExpiryPolicy): Promise<PasswordExpiryPolicy> => { | ||||
|         if (this.instance) { | ||||
|             return new PoliciesApi(DEFAULT_CONFIG).policiesPasswordExpiryUpdate({ | ||||
|                 policyUuid: this.instance.pk || "", | ||||
|                 passwordExpiryPolicyRequest: data, | ||||
|             }); | ||||
|         } else { | ||||
|             return new PoliciesApi(DEFAULT_CONFIG).policiesPasswordExpiryCreate({ | ||||
|                 passwordExpiryPolicyRequest: data, | ||||
|             }); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     renderForm(): TemplateResult { | ||||
|         return html`<form class="pf-c-form pf-m-horizontal"> | ||||
|             <div class="form-help-text"> | ||||
|                 ${t`Checks if the request's user's password has been changed in the last x days, and denys based on settings.`} | ||||
|             </div> | ||||
|             <ak-form-element-horizontal label=${t`Name`} ?required=${true} name="name"> | ||||
|                 <input | ||||
|                     type="text" | ||||
|                     value="${ifDefined(this.instance?.name || "")}" | ||||
|                     class="pf-c-form-control" | ||||
|                     required | ||||
|                 /> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-element-horizontal name="executionLogging"> | ||||
|                 <div class="pf-c-check"> | ||||
|                     <input | ||||
|                         type="checkbox" | ||||
|                         class="pf-c-check__input" | ||||
|                         ?checked=${first(this.instance?.executionLogging, false)} | ||||
|                     /> | ||||
|                     <label class="pf-c-check__label"> ${t`Execution logging`} </label> | ||||
|                 </div> | ||||
|                 <p class="pf-c-form__helper-text"> | ||||
|                     ${t`When this option is enabled, all executions of this policy will be logged. By default, only execution errors are logged.`} | ||||
|                 </p> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-group .expanded=${true}> | ||||
|                 <span slot="header"> ${t`Policy-specific settings`} </span> | ||||
|                 <div slot="body" class="pf-c-form"> | ||||
|                     <ak-form-element-horizontal | ||||
|                         label=${t`Maximum age (in days)`} | ||||
|                         ?required=${true} | ||||
|                         name="days" | ||||
|                     > | ||||
|                         <input | ||||
|                             type="number" | ||||
|                             value="${ifDefined(this.instance?.days || "")}" | ||||
|                             class="pf-c-form-control" | ||||
|                             required | ||||
|                         /> | ||||
|                     </ak-form-element-horizontal> | ||||
|                     <ak-form-element-horizontal name="denyOnly"> | ||||
|                         <div class="pf-c-check"> | ||||
|                             <input | ||||
|                                 type="checkbox" | ||||
|                                 class="pf-c-check__input" | ||||
|                                 ?checked=${first(this.instance?.denyOnly, false)} | ||||
|                             /> | ||||
|                             <label class="pf-c-check__label"> | ||||
|                                 ${t`Only fail the policy, don't invalidate user's password.`} | ||||
|                             </label> | ||||
|                         </div> | ||||
|                     </ak-form-element-horizontal> | ||||
|                 </div> | ||||
|             </ak-form-group> | ||||
|         </form>`; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										98
									
								
								web/src/admin/policies/expression/ExpressionPolicyForm.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										98
									
								
								web/src/admin/policies/expression/ExpressionPolicyForm.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,98 @@ | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { first } from "@goauthentik/common/utils"; | ||||
| import "@goauthentik/elements/CodeMirror"; | ||||
| import "@goauthentik/elements/forms/FormGroup"; | ||||
| import "@goauthentik/elements/forms/HorizontalFormElement"; | ||||
| import { ModelForm } from "@goauthentik/elements/forms/ModelForm"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { TemplateResult, html } from "lit"; | ||||
| import { customElement } from "lit/decorators.js"; | ||||
| import { ifDefined } from "lit/directives/if-defined.js"; | ||||
|  | ||||
| import { ExpressionPolicy, PoliciesApi } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-policy-expression-form") | ||||
| export class ExpressionPolicyForm extends ModelForm<ExpressionPolicy, string> { | ||||
|     loadInstance(pk: string): Promise<ExpressionPolicy> { | ||||
|         return new PoliciesApi(DEFAULT_CONFIG).policiesExpressionRetrieve({ | ||||
|             policyUuid: pk, | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     getSuccessMessage(): string { | ||||
|         if (this.instance) { | ||||
|             return t`Successfully updated policy.`; | ||||
|         } else { | ||||
|             return t`Successfully created policy.`; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     send = (data: ExpressionPolicy): Promise<ExpressionPolicy> => { | ||||
|         if (this.instance) { | ||||
|             return new PoliciesApi(DEFAULT_CONFIG).policiesExpressionUpdate({ | ||||
|                 policyUuid: this.instance.pk || "", | ||||
|                 expressionPolicyRequest: data, | ||||
|             }); | ||||
|         } else { | ||||
|             return new PoliciesApi(DEFAULT_CONFIG).policiesExpressionCreate({ | ||||
|                 expressionPolicyRequest: data, | ||||
|             }); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     renderForm(): TemplateResult { | ||||
|         return html`<form class="pf-c-form pf-m-horizontal"> | ||||
|             <div class="form-help-text"> | ||||
|                 ${t`Executes the python snippet to determine whether to allow or deny a request.`} | ||||
|             </div> | ||||
|             <ak-form-element-horizontal label=${t`Name`} ?required=${true} name="name"> | ||||
|                 <input | ||||
|                     type="text" | ||||
|                     value="${ifDefined(this.instance?.name || "")}" | ||||
|                     class="pf-c-form-control" | ||||
|                     required | ||||
|                 /> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-element-horizontal name="executionLogging"> | ||||
|                 <div class="pf-c-check"> | ||||
|                     <input | ||||
|                         type="checkbox" | ||||
|                         class="pf-c-check__input" | ||||
|                         ?checked=${first(this.instance?.executionLogging, false)} | ||||
|                     /> | ||||
|                     <label class="pf-c-check__label"> ${t`Execution logging`} </label> | ||||
|                 </div> | ||||
|                 <p class="pf-c-form__helper-text"> | ||||
|                     ${t`When this option is enabled, all executions of this policy will be logged. By default, only execution errors are logged.`} | ||||
|                 </p> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-group .expanded=${true}> | ||||
|                 <span slot="header"> ${t`Policy-specific settings`} </span> | ||||
|                 <div slot="body" class="pf-c-form"> | ||||
|                     <ak-form-element-horizontal | ||||
|                         label=${t`Expression`} | ||||
|                         ?required=${true} | ||||
|                         name="expression" | ||||
|                     > | ||||
|                         <ak-codemirror | ||||
|                             mode="python" | ||||
|                             value="${ifDefined(this.instance?.expression)}" | ||||
|                         > | ||||
|                         </ak-codemirror> | ||||
|                         <p class="pf-c-form__helper-text"> | ||||
|                             ${t`Expression using Python.`} | ||||
|                             <a | ||||
|                                 target="_blank" | ||||
|                                 href="https://goauthentik.io/docs/policies/expression?utm_source=authentik" | ||||
|                             > | ||||
|                                 ${t`See documentation for a list of all variables.`} | ||||
|                             </a> | ||||
|                         </p> | ||||
|                     </ak-form-element-horizontal> | ||||
|                 </div> | ||||
|             </ak-form-group> | ||||
|         </form>`; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										108
									
								
								web/src/admin/policies/hibp/HaveIBeenPwnedPolicyForm.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								web/src/admin/policies/hibp/HaveIBeenPwnedPolicyForm.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,108 @@ | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { first } from "@goauthentik/common/utils"; | ||||
| import "@goauthentik/elements/forms/FormGroup"; | ||||
| import "@goauthentik/elements/forms/HorizontalFormElement"; | ||||
| import { ModelForm } from "@goauthentik/elements/forms/ModelForm"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { TemplateResult, html } from "lit"; | ||||
| import { customElement } from "lit/decorators.js"; | ||||
| import { ifDefined } from "lit/directives/if-defined.js"; | ||||
|  | ||||
| import { HaveIBeenPwendPolicy, PoliciesApi } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-policy-hibp-form") | ||||
| export class HaveIBeenPwnedPolicyForm extends ModelForm<HaveIBeenPwendPolicy, string> { | ||||
|     loadInstance(pk: string): Promise<HaveIBeenPwendPolicy> { | ||||
|         return new PoliciesApi(DEFAULT_CONFIG).policiesHaveibeenpwnedRetrieve({ | ||||
|             policyUuid: pk, | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     getSuccessMessage(): string { | ||||
|         if (this.instance) { | ||||
|             return t`Successfully updated policy.`; | ||||
|         } else { | ||||
|             return t`Successfully created policy.`; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     send = (data: HaveIBeenPwendPolicy): Promise<HaveIBeenPwendPolicy> => { | ||||
|         if (this.instance) { | ||||
|             return new PoliciesApi(DEFAULT_CONFIG).policiesHaveibeenpwnedUpdate({ | ||||
|                 policyUuid: this.instance.pk || "", | ||||
|                 haveIBeenPwendPolicyRequest: data, | ||||
|             }); | ||||
|         } else { | ||||
|             return new PoliciesApi(DEFAULT_CONFIG).policiesHaveibeenpwnedCreate({ | ||||
|                 haveIBeenPwendPolicyRequest: data, | ||||
|             }); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     renderForm(): TemplateResult { | ||||
|         return html`<form class="pf-c-form pf-m-horizontal"> | ||||
|             <div class="form-help-text"> | ||||
|                 ${t`Checks a value from the policy request against the Have I been Pwned API, and denys the request based upon that. | ||||
|                 Note that only a part of the hash of the password is sent, the full comparison is done clientside.`} | ||||
|             </div> | ||||
|             <ak-form-element-horizontal label=${t`Name`} ?required=${true} name="name"> | ||||
|                 <input | ||||
|                     type="text" | ||||
|                     value="${ifDefined(this.instance?.name || "")}" | ||||
|                     class="pf-c-form-control" | ||||
|                     required | ||||
|                 /> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-element-horizontal name="executionLogging"> | ||||
|                 <div class="pf-c-check"> | ||||
|                     <input | ||||
|                         type="checkbox" | ||||
|                         class="pf-c-check__input" | ||||
|                         ?checked=${first(this.instance?.executionLogging, false)} | ||||
|                     /> | ||||
|                     <label class="pf-c-check__label"> ${t`Execution logging`} </label> | ||||
|                 </div> | ||||
|                 <p class="pf-c-form__helper-text"> | ||||
|                     ${t`When this option is enabled, all executions of this policy will be logged. By default, only execution errors are logged.`} | ||||
|                 </p> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-group .expanded=${true}> | ||||
|                 <span slot="header"> ${t`Policy-specific settings`} </span> | ||||
|                 <div slot="body" class="pf-c-form"> | ||||
|                     <ak-form-element-horizontal | ||||
|                         label=${t`Password field`} | ||||
|                         ?required=${true} | ||||
|                         name="passwordField" | ||||
|                     > | ||||
|                         <input | ||||
|                             type="text" | ||||
|                             value="${ifDefined(this.instance?.passwordField || "password")}" | ||||
|                             class="pf-c-form-control" | ||||
|                             required | ||||
|                         /> | ||||
|                         <p class="pf-c-form__helper-text"> | ||||
|                             ${t`Field key to check, field keys defined in Prompt stages are available.`} | ||||
|                         </p> | ||||
|                     </ak-form-element-horizontal> | ||||
|                     <ak-form-element-horizontal | ||||
|                         label=${t`Allowed count`} | ||||
|                         ?required=${true} | ||||
|                         name="allowedCount" | ||||
|                     > | ||||
|                         <input | ||||
|                             type="number" | ||||
|                             value="${first(this.instance?.allowedCount, 0)}" | ||||
|                             class="pf-c-form-control" | ||||
|                             required | ||||
|                         /> | ||||
|                         <p class="pf-c-form__helper-text"> | ||||
|                             ${t`Allow up to N occurrences in the HIBP database.`} | ||||
|                         </p> | ||||
|                     </ak-form-element-horizontal> | ||||
|                 </div> | ||||
|             </ak-form-group> | ||||
|         </form>`; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										188
									
								
								web/src/admin/policies/password/PasswordPolicyForm.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										188
									
								
								web/src/admin/policies/password/PasswordPolicyForm.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,188 @@ | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { first } from "@goauthentik/common/utils"; | ||||
| import "@goauthentik/elements/forms/FormGroup"; | ||||
| import "@goauthentik/elements/forms/HorizontalFormElement"; | ||||
| import { ModelForm } from "@goauthentik/elements/forms/ModelForm"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { TemplateResult, html } from "lit"; | ||||
| import { customElement } from "lit/decorators.js"; | ||||
| import { ifDefined } from "lit/directives/if-defined.js"; | ||||
|  | ||||
| import { PasswordPolicy, PoliciesApi } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-policy-password-form") | ||||
| export class PasswordPolicyForm extends ModelForm<PasswordPolicy, string> { | ||||
|     loadInstance(pk: string): Promise<PasswordPolicy> { | ||||
|         return new PoliciesApi(DEFAULT_CONFIG).policiesPasswordRetrieve({ | ||||
|             policyUuid: pk, | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     getSuccessMessage(): string { | ||||
|         if (this.instance) { | ||||
|             return t`Successfully updated policy.`; | ||||
|         } else { | ||||
|             return t`Successfully created policy.`; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     send = (data: PasswordPolicy): Promise<PasswordPolicy> => { | ||||
|         if (this.instance) { | ||||
|             return new PoliciesApi(DEFAULT_CONFIG).policiesPasswordUpdate({ | ||||
|                 policyUuid: this.instance.pk || "", | ||||
|                 passwordPolicyRequest: data, | ||||
|             }); | ||||
|         } else { | ||||
|             return new PoliciesApi(DEFAULT_CONFIG).policiesPasswordCreate({ | ||||
|                 passwordPolicyRequest: data, | ||||
|             }); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     renderForm(): TemplateResult { | ||||
|         return html`<form class="pf-c-form pf-m-horizontal"> | ||||
|             <div class="form-help-text"> | ||||
|                 ${t`Checks the value from the policy request against several rules, mostly used to ensure password strength.`} | ||||
|             </div> | ||||
|             <ak-form-element-horizontal label=${t`Name`} ?required=${true} name="name"> | ||||
|                 <input | ||||
|                     type="text" | ||||
|                     value="${ifDefined(this.instance?.name || "")}" | ||||
|                     class="pf-c-form-control" | ||||
|                     required | ||||
|                 /> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-element-horizontal name="executionLogging"> | ||||
|                 <div class="pf-c-check"> | ||||
|                     <input | ||||
|                         type="checkbox" | ||||
|                         class="pf-c-check__input" | ||||
|                         ?checked=${first(this.instance?.executionLogging, false)} | ||||
|                     /> | ||||
|                     <label class="pf-c-check__label"> ${t`Execution logging`} </label> | ||||
|                 </div> | ||||
|                 <p class="pf-c-form__helper-text"> | ||||
|                     ${t`When this option is enabled, all executions of this policy will be logged. By default, only execution errors are logged.`} | ||||
|                 </p> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-group .expanded=${true}> | ||||
|                 <span slot="header"> ${t`Policy-specific settings`} </span> | ||||
|                 <div slot="body" class="pf-c-form"> | ||||
|                     <ak-form-element-horizontal | ||||
|                         label=${t`Password field`} | ||||
|                         ?required=${true} | ||||
|                         name="passwordField" | ||||
|                     > | ||||
|                         <input | ||||
|                             type="text" | ||||
|                             value="${ifDefined(this.instance?.passwordField || "password")}" | ||||
|                             class="pf-c-form-control" | ||||
|                             required | ||||
|                         /> | ||||
|                         <p class="pf-c-form__helper-text"> | ||||
|                             ${t`Field key to check, field keys defined in Prompt stages are available.`} | ||||
|                         </p> | ||||
|                     </ak-form-element-horizontal> | ||||
|  | ||||
|                     <ak-form-element-horizontal | ||||
|                         label=${t`Minimum length`} | ||||
|                         ?required=${true} | ||||
|                         name="lengthMin" | ||||
|                     > | ||||
|                         <input | ||||
|                             type="number" | ||||
|                             value="${first(this.instance?.lengthMin, 10)}" | ||||
|                             class="pf-c-form-control" | ||||
|                             required | ||||
|                         /> | ||||
|                     </ak-form-element-horizontal> | ||||
|                     <ak-form-element-horizontal | ||||
|                         label=${t`Minimum amount of Uppercase Characters`} | ||||
|                         ?required=${true} | ||||
|                         name="amountUppercase" | ||||
|                     > | ||||
|                         <input | ||||
|                             type="number" | ||||
|                             value="${first(this.instance?.amountUppercase, 2)}" | ||||
|                             class="pf-c-form-control" | ||||
|                             required | ||||
|                         /> | ||||
|                     </ak-form-element-horizontal> | ||||
|                     <ak-form-element-horizontal | ||||
|                         label=${t`Minimum amount of Lowercase Characters`} | ||||
|                         ?required=${true} | ||||
|                         name="amountLowercase" | ||||
|                     > | ||||
|                         <input | ||||
|                             type="number" | ||||
|                             value="${first(this.instance?.amountLowercase, 2)}" | ||||
|                             class="pf-c-form-control" | ||||
|                             required | ||||
|                         /> | ||||
|                     </ak-form-element-horizontal> | ||||
|                     <ak-form-element-horizontal | ||||
|                         label=${t`Minimum amount of Digits`} | ||||
|                         ?required=${true} | ||||
|                         name="amountDigits" | ||||
|                     > | ||||
|                         <input | ||||
|                             type="number" | ||||
|                             value="${first(this.instance?.amountDigits, 2)}" | ||||
|                             class="pf-c-form-control" | ||||
|                             required | ||||
|                         /> | ||||
|                     </ak-form-element-horizontal> | ||||
|                     <ak-form-element-horizontal | ||||
|                         label=${t`Minimum amount of Symbols Characters`} | ||||
|                         ?required=${true} | ||||
|                         name="amountSymbols" | ||||
|                     > | ||||
|                         <input | ||||
|                             type="number" | ||||
|                             value="${first(this.instance?.amountSymbols, 2)}" | ||||
|                             class="pf-c-form-control" | ||||
|                             required | ||||
|                         /> | ||||
|                     </ak-form-element-horizontal> | ||||
|                     <ak-form-element-horizontal | ||||
|                         label=${t`Error message`} | ||||
|                         ?required=${true} | ||||
|                         name="errorMessage" | ||||
|                     > | ||||
|                         <input | ||||
|                             type="text" | ||||
|                             value="${ifDefined(this.instance?.errorMessage)}" | ||||
|                             class="pf-c-form-control" | ||||
|                             required | ||||
|                         /> | ||||
|                     </ak-form-element-horizontal> | ||||
|                 </div> | ||||
|             </ak-form-group> | ||||
|             <ak-form-group> | ||||
|                 <span slot="header"> ${t`Advanced settings`} </span> | ||||
|                 <div slot="body" class="pf-c-form"> | ||||
|                     <ak-form-element-horizontal | ||||
|                         label=${t`Symbol charset`} | ||||
|                         ?required=${true} | ||||
|                         name="symbolCharset" | ||||
|                     > | ||||
|                         <input | ||||
|                             type="text" | ||||
|                             value="${ifDefined( | ||||
|                                 this.instance?.symbolCharset || | ||||
|                                     "!\\\"#$%&'()*+,-./:;<=>?@[]^_`{|}~ ", | ||||
|                             )}" | ||||
|                             class="pf-c-form-control" | ||||
|                             required | ||||
|                         /> | ||||
|                         <p class="pf-c-form__helper-text"> | ||||
|                             ${t`Characters which are considered as symbols.`} | ||||
|                         </p> | ||||
|                     </ak-form-element-horizontal> | ||||
|                 </div> | ||||
|             </ak-form-group> | ||||
|         </form>`; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										90
									
								
								web/src/admin/policies/reputation/ReputationListPage.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								web/src/admin/policies/reputation/ReputationListPage.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,90 @@ | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { uiConfig } from "@goauthentik/common/ui/config"; | ||||
| import "@goauthentik/elements/buttons/ModalButton"; | ||||
| import "@goauthentik/elements/buttons/SpinnerButton"; | ||||
| import "@goauthentik/elements/forms/DeleteBulkForm"; | ||||
| import "@goauthentik/elements/forms/ModalForm"; | ||||
| import { PaginatedResponse } from "@goauthentik/elements/table/Table"; | ||||
| import { TableColumn } from "@goauthentik/elements/table/Table"; | ||||
| import { TablePage } from "@goauthentik/elements/table/TablePage"; | ||||
| import getUnicodeFlagIcon from "country-flag-icons/unicode"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { TemplateResult, html } from "lit"; | ||||
| import { customElement, property } from "lit/decorators.js"; | ||||
|  | ||||
| import { PoliciesApi, Reputation } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-policy-reputation-list") | ||||
| export class ReputationListPage extends TablePage<Reputation> { | ||||
|     searchEnabled(): boolean { | ||||
|         return true; | ||||
|     } | ||||
|     pageTitle(): string { | ||||
|         return t`Reputation scores`; | ||||
|     } | ||||
|     pageDescription(): string { | ||||
|         return t`Reputation for IP and user identifiers. Scores are decreased for each failed login and increased for each successful login.`; | ||||
|     } | ||||
|     pageIcon(): string { | ||||
|         return "fa fa-ban"; | ||||
|     } | ||||
|  | ||||
|     @property() | ||||
|     order = "identifier"; | ||||
|  | ||||
|     checkbox = true; | ||||
|  | ||||
|     async apiEndpoint(page: number): Promise<PaginatedResponse<Reputation>> { | ||||
|         return new PoliciesApi(DEFAULT_CONFIG).policiesReputationScoresList({ | ||||
|             ordering: this.order, | ||||
|             page: page, | ||||
|             pageSize: (await uiConfig()).pagination.perPage, | ||||
|             search: this.search || "", | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     columns(): TableColumn[] { | ||||
|         return [ | ||||
|             new TableColumn(t`Identifier`, "identifier"), | ||||
|             new TableColumn(t`IP`, "ip"), | ||||
|             new TableColumn(t`Score`, "score"), | ||||
|             new TableColumn(t`Updated`, "updated"), | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     renderToolbarSelected(): TemplateResult { | ||||
|         const disabled = this.selectedElements.length < 1; | ||||
|         return html`<ak-forms-delete-bulk | ||||
|             objectLabel=${t`Reputation`} | ||||
|             .objects=${this.selectedElements} | ||||
|             .usedBy=${(item: Reputation) => { | ||||
|                 return new PoliciesApi(DEFAULT_CONFIG).policiesReputationScoresUsedByList({ | ||||
|                     reputationUuid: item.pk || "", | ||||
|                 }); | ||||
|             }} | ||||
|             .delete=${(item: Reputation) => { | ||||
|                 return new PoliciesApi(DEFAULT_CONFIG).policiesReputationScoresDestroy({ | ||||
|                     reputationUuid: item.pk || "", | ||||
|                 }); | ||||
|             }} | ||||
|         > | ||||
|             <button ?disabled=${disabled} slot="trigger" class="pf-c-button pf-m-danger"> | ||||
|                 ${t`Delete`} | ||||
|             </button> | ||||
|         </ak-forms-delete-bulk>`; | ||||
|     } | ||||
|  | ||||
|     row(item: Reputation): TemplateResult[] { | ||||
|         return [ | ||||
|             html`${item.identifier}`, | ||||
|             html`${item.ipGeoData?.country | ||||
|                 ? html` ${getUnicodeFlagIcon(item.ipGeoData.country)} ` | ||||
|                 : html``} | ||||
|             ${item.ip}`, | ||||
|             html`${item.score}`, | ||||
|             html`${item.updated.toLocaleString()}`, | ||||
|         ]; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										118
									
								
								web/src/admin/policies/reputation/ReputationPolicyForm.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										118
									
								
								web/src/admin/policies/reputation/ReputationPolicyForm.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,118 @@ | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { first } from "@goauthentik/common/utils"; | ||||
| import "@goauthentik/elements/forms/FormGroup"; | ||||
| import "@goauthentik/elements/forms/HorizontalFormElement"; | ||||
| import { ModelForm } from "@goauthentik/elements/forms/ModelForm"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { TemplateResult, html } from "lit"; | ||||
| import { customElement } from "lit/decorators.js"; | ||||
| import { ifDefined } from "lit/directives/if-defined.js"; | ||||
|  | ||||
| import { PoliciesApi, ReputationPolicy } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-policy-reputation-form") | ||||
| export class ReputationPolicyForm extends ModelForm<ReputationPolicy, string> { | ||||
|     loadInstance(pk: string): Promise<ReputationPolicy> { | ||||
|         return new PoliciesApi(DEFAULT_CONFIG).policiesReputationRetrieve({ | ||||
|             policyUuid: pk, | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     getSuccessMessage(): string { | ||||
|         if (this.instance) { | ||||
|             return t`Successfully updated policy.`; | ||||
|         } else { | ||||
|             return t`Successfully created policy.`; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     send = (data: ReputationPolicy): Promise<ReputationPolicy> => { | ||||
|         if (this.instance) { | ||||
|             return new PoliciesApi(DEFAULT_CONFIG).policiesReputationUpdate({ | ||||
|                 policyUuid: this.instance.pk || "", | ||||
|                 reputationPolicyRequest: data, | ||||
|             }); | ||||
|         } else { | ||||
|             return new PoliciesApi(DEFAULT_CONFIG).policiesReputationCreate({ | ||||
|                 reputationPolicyRequest: data, | ||||
|             }); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     renderForm(): TemplateResult { | ||||
|         return html`<form class="pf-c-form pf-m-horizontal"> | ||||
|             <div class="form-help-text"> | ||||
|                 ${t`Allows/denys requests based on the users and/or the IPs reputation.`} | ||||
|             </div> | ||||
|             <div class="form-help-text"> | ||||
|                 ${t`Invalid login attempts will decrease the score for the client's IP, and the | ||||
|                 username they are attempting to login as, by one.`} | ||||
|             </div> | ||||
|             <div class="form-help-text"> | ||||
|                 ${t`The policy passes when the reputation score is below the threshold, and | ||||
|                 doesn't pass when either or both of the selected options are equal or above the | ||||
|                 threshold.`} | ||||
|             </div> | ||||
|             <ak-form-element-horizontal label=${t`Name`} ?required=${true} name="name"> | ||||
|                 <input | ||||
|                     type="text" | ||||
|                     value="${ifDefined(this.instance?.name || "")}" | ||||
|                     class="pf-c-form-control" | ||||
|                     required | ||||
|                 /> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-element-horizontal name="executionLogging"> | ||||
|                 <div class="pf-c-check"> | ||||
|                     <input | ||||
|                         type="checkbox" | ||||
|                         class="pf-c-check__input" | ||||
|                         ?checked=${first(this.instance?.executionLogging, false)} | ||||
|                     /> | ||||
|                     <label class="pf-c-check__label"> ${t`Execution logging`} </label> | ||||
|                 </div> | ||||
|                 <p class="pf-c-form__helper-text"> | ||||
|                     ${t`When this option is enabled, all executions of this policy will be logged. By default, only execution errors are logged.`} | ||||
|                 </p> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-group .expanded=${true}> | ||||
|                 <span slot="header"> ${t`Policy-specific settings`} </span> | ||||
|                 <div slot="body" class="pf-c-form"> | ||||
|                     <ak-form-element-horizontal name="checkIp"> | ||||
|                         <div class="pf-c-check"> | ||||
|                             <input | ||||
|                                 type="checkbox" | ||||
|                                 class="pf-c-check__input" | ||||
|                                 ?checked=${first(this.instance?.checkIp, false)} | ||||
|                             /> | ||||
|                             <label class="pf-c-check__label"> ${t`Check IP`} </label> | ||||
|                         </div> | ||||
|                     </ak-form-element-horizontal> | ||||
|                     <ak-form-element-horizontal name="checkUsername"> | ||||
|                         <div class="pf-c-check"> | ||||
|                             <input | ||||
|                                 type="checkbox" | ||||
|                                 class="pf-c-check__input" | ||||
|                                 ?checked=${first(this.instance?.checkUsername, false)} | ||||
|                             /> | ||||
|                             <label class="pf-c-check__label"> ${t`Check Username`} </label> | ||||
|                         </div> | ||||
|                     </ak-form-element-horizontal> | ||||
|                     <ak-form-element-horizontal | ||||
|                         label=${t`Threshold`} | ||||
|                         ?required=${true} | ||||
|                         name="threshold" | ||||
|                     > | ||||
|                         <input | ||||
|                             type="number" | ||||
|                             value="${ifDefined(this.instance?.threshold || -5)}" | ||||
|                             class="pf-c-form-control" | ||||
|                             required | ||||
|                         /> | ||||
|                     </ak-form-element-horizontal> | ||||
|                 </div> | ||||
|             </ak-form-group> | ||||
|         </form>`; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										83
									
								
								web/src/admin/property-mappings/PropertyMappingLDAPForm.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								web/src/admin/property-mappings/PropertyMappingLDAPForm.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,83 @@ | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import "@goauthentik/elements/CodeMirror"; | ||||
| import "@goauthentik/elements/forms/HorizontalFormElement"; | ||||
| import { ModelForm } from "@goauthentik/elements/forms/ModelForm"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { TemplateResult, html } from "lit"; | ||||
| import { customElement } from "lit/decorators.js"; | ||||
| import { ifDefined } from "lit/directives/if-defined.js"; | ||||
|  | ||||
| import { LDAPPropertyMapping, PropertymappingsApi } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-property-mapping-ldap-form") | ||||
| export class PropertyMappingLDAPForm extends ModelForm<LDAPPropertyMapping, string> { | ||||
|     loadInstance(pk: string): Promise<LDAPPropertyMapping> { | ||||
|         return new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsLdapRetrieve({ | ||||
|             pmUuid: pk, | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     getSuccessMessage(): string { | ||||
|         if (this.instance) { | ||||
|             return t`Successfully updated mapping.`; | ||||
|         } else { | ||||
|             return t`Successfully created mapping.`; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     send = (data: LDAPPropertyMapping): Promise<LDAPPropertyMapping> => { | ||||
|         if (this.instance) { | ||||
|             return new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsLdapUpdate({ | ||||
|                 pmUuid: this.instance.pk || "", | ||||
|                 lDAPPropertyMappingRequest: data, | ||||
|             }); | ||||
|         } else { | ||||
|             return new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsLdapCreate({ | ||||
|                 lDAPPropertyMappingRequest: 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="${ifDefined(this.instance?.name)}" | ||||
|                     class="pf-c-form-control" | ||||
|                     required | ||||
|                 /> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-element-horizontal | ||||
|                 label=${t`Object field`} | ||||
|                 ?required=${true} | ||||
|                 name="objectField" | ||||
|             > | ||||
|                 <input | ||||
|                     type="text" | ||||
|                     value="${ifDefined(this.instance?.objectField)}" | ||||
|                     class="pf-c-form-control" | ||||
|                     required | ||||
|                 /> | ||||
|                 <p class="pf-c-form__helper-text"> | ||||
|                     ${t`Field of the user object this value is written to.`} | ||||
|                 </p> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-element-horizontal label=${t`Expression`} ?required=${true} name="expression"> | ||||
|                 <ak-codemirror mode="python" value="${ifDefined(this.instance?.expression)}"> | ||||
|                 </ak-codemirror> | ||||
|                 <p class="pf-c-form__helper-text"> | ||||
|                     ${t`Expression using Python.`} | ||||
|                     <a | ||||
|                         target="_blank" | ||||
|                         href="https://goauthentik.io/docs/property-mappings/expression?utm_source=authentik" | ||||
|                     > | ||||
|                         ${t`See documentation for a list of all variables.`} | ||||
|                     </a> | ||||
|                 </p> | ||||
|             </ak-form-element-horizontal> | ||||
|         </form>`; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										157
									
								
								web/src/admin/property-mappings/PropertyMappingListPage.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										157
									
								
								web/src/admin/property-mappings/PropertyMappingListPage.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,157 @@ | ||||
| import "@goauthentik/admin/property-mappings/PropertyMappingLDAPForm"; | ||||
| import "@goauthentik/admin/property-mappings/PropertyMappingNotification"; | ||||
| import "@goauthentik/admin/property-mappings/PropertyMappingSAMLForm"; | ||||
| import "@goauthentik/admin/property-mappings/PropertyMappingScopeForm"; | ||||
| import "@goauthentik/admin/property-mappings/PropertyMappingTestForm"; | ||||
| import "@goauthentik/admin/property-mappings/PropertyMappingWizard"; | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { uiConfig } from "@goauthentik/common/ui/config"; | ||||
| import { groupBy } from "@goauthentik/common/utils"; | ||||
| import "@goauthentik/elements/forms/DeleteBulkForm"; | ||||
| import "@goauthentik/elements/forms/ModalForm"; | ||||
| import "@goauthentik/elements/forms/ProxyForm"; | ||||
| import { getURLParam, updateURLParams } from "@goauthentik/elements/router/RouteMatch"; | ||||
| import { PaginatedResponse } from "@goauthentik/elements/table/Table"; | ||||
| import { TableColumn } from "@goauthentik/elements/table/Table"; | ||||
| import { TablePage } from "@goauthentik/elements/table/TablePage"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { TemplateResult, html } from "lit"; | ||||
| import { customElement, property } from "lit/decorators.js"; | ||||
| import { ifDefined } from "lit/directives/if-defined.js"; | ||||
|  | ||||
| import { PropertyMapping, PropertymappingsApi } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-property-mapping-list") | ||||
| export class PropertyMappingListPage extends TablePage<PropertyMapping> { | ||||
|     searchEnabled(): boolean { | ||||
|         return true; | ||||
|     } | ||||
|     pageTitle(): string { | ||||
|         return t`Property Mappings`; | ||||
|     } | ||||
|     pageDescription(): string { | ||||
|         return t`Control how authentik exposes and interprets information.`; | ||||
|     } | ||||
|     pageIcon(): string { | ||||
|         return "pf-icon pf-icon-blueprint"; | ||||
|     } | ||||
|  | ||||
|     checkbox = true; | ||||
|  | ||||
|     @property() | ||||
|     order = "name"; | ||||
|  | ||||
|     @property({ type: Boolean }) | ||||
|     hideManaged = getURLParam<boolean>("hideManaged", true); | ||||
|  | ||||
|     async apiEndpoint(page: number): Promise<PaginatedResponse<PropertyMapping>> { | ||||
|         return new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsAllList({ | ||||
|             ordering: this.order, | ||||
|             page: page, | ||||
|             pageSize: (await uiConfig()).pagination.perPage, | ||||
|             search: this.search || "", | ||||
|             managedIsnull: this.hideManaged ? true : undefined, | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     groupBy(items: PropertyMapping[]): [string, PropertyMapping[]][] { | ||||
|         return groupBy(items, (mapping) => mapping.verboseNamePlural); | ||||
|     } | ||||
|  | ||||
|     columns(): TableColumn[] { | ||||
|         return [ | ||||
|             new TableColumn(t`Name`, "name"), | ||||
|             new TableColumn(t`Type`, "type"), | ||||
|             new TableColumn(t`Actions`), | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     renderToolbarSelected(): TemplateResult { | ||||
|         const disabled = this.selectedElements.length < 1; | ||||
|         return html`<ak-forms-delete-bulk | ||||
|             objectLabel=${t`Property Mapping(s)`} | ||||
|             .objects=${this.selectedElements} | ||||
|             .usedBy=${(item: PropertyMapping) => { | ||||
|                 return new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsAllUsedByList({ | ||||
|                     pmUuid: item.pk, | ||||
|                 }); | ||||
|             }} | ||||
|             .delete=${(item: PropertyMapping) => { | ||||
|                 return new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsAllDestroy({ | ||||
|                     pmUuid: item.pk, | ||||
|                 }); | ||||
|             }} | ||||
|         > | ||||
|             <button ?disabled=${disabled} slot="trigger" class="pf-c-button pf-m-danger"> | ||||
|                 ${t`Delete`} | ||||
|             </button> | ||||
|         </ak-forms-delete-bulk>`; | ||||
|     } | ||||
|  | ||||
|     row(item: PropertyMapping): TemplateResult[] { | ||||
|         return [ | ||||
|             html`${item.name}`, | ||||
|             html`${item.verboseName}`, | ||||
|             html` <ak-forms-modal> | ||||
|                     <span slot="submit"> ${t`Update`} </span> | ||||
|                     <span slot="header"> ${t`Update ${item.verboseName}`} </span> | ||||
|                     <ak-proxy-form | ||||
|                         slot="form" | ||||
|                         .args=${{ | ||||
|                             instancePk: item.pk, | ||||
|                         }} | ||||
|                         type=${ifDefined(item.component)} | ||||
|                     > | ||||
|                     </ak-proxy-form> | ||||
|                     <button slot="trigger" class="pf-c-button pf-m-plain"> | ||||
|                         <i class="fas fa-edit"></i> | ||||
|                     </button> | ||||
|                 </ak-forms-modal> | ||||
|                 <ak-forms-modal .closeAfterSuccessfulSubmit=${false}> | ||||
|                     <span slot="submit"> ${t`Test`} </span> | ||||
|                     <span slot="header"> ${t`Test Property Mapping`} </span> | ||||
|                     <ak-property-mapping-test-form slot="form" .mapping=${item}> | ||||
|                     </ak-property-mapping-test-form> | ||||
|                     <button slot="trigger" class="pf-c-button pf-m-plain"> | ||||
|                         <i class="fas fa-vial" aria-hidden="true"></i> | ||||
|                     </button> | ||||
|                 </ak-forms-modal>`, | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     renderObjectCreate(): TemplateResult { | ||||
|         return html`<ak-property-mapping-wizard></ak-property-mapping-wizard> `; | ||||
|     } | ||||
|  | ||||
|     renderToolbarAfter(): TemplateResult { | ||||
|         return html`  | ||||
|             <div class="pf-c-toolbar__group pf-m-filter-group"> | ||||
|                 <div class="pf-c-toolbar__item pf-m-search-filter"> | ||||
|                     <div class="pf-c-input-group"> | ||||
|                         <div class="pf-c-check"> | ||||
|                             <input | ||||
|                                 class="pf-c-check__input" | ||||
|                                 type="checkbox" | ||||
|                                 id="hide-managed" | ||||
|                                 name="hide-managed" | ||||
|                                 ?checked=${this.hideManaged} | ||||
|                                 @change=${() => { | ||||
|                                     this.hideManaged = !this.hideManaged; | ||||
|                                     this.page = 1; | ||||
|                                     this.fetch(); | ||||
|                                     updateURLParams({ | ||||
|                                         hideManaged: this.hideManaged, | ||||
|                                     }); | ||||
|                                 }} | ||||
|                             /> | ||||
|                             <label class="pf-c-check__label" for="hide-managed" | ||||
|                                 >${t`Hide managed mappings`}</label | ||||
|                             > | ||||
|                         </div> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </div>`; | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,68 @@ | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import "@goauthentik/elements/CodeMirror"; | ||||
| import "@goauthentik/elements/forms/HorizontalFormElement"; | ||||
| import { ModelForm } from "@goauthentik/elements/forms/ModelForm"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { TemplateResult, html } from "lit"; | ||||
| import { customElement } from "lit/decorators.js"; | ||||
| import { ifDefined } from "lit/directives/if-defined.js"; | ||||
|  | ||||
| import { NotificationWebhookMapping, PropertymappingsApi } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-property-mapping-notification-form") | ||||
| export class PropertyMappingNotification extends ModelForm<NotificationWebhookMapping, string> { | ||||
|     loadInstance(pk: string): Promise<NotificationWebhookMapping> { | ||||
|         return new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsNotificationRetrieve({ | ||||
|             pmUuid: pk, | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     getSuccessMessage(): string { | ||||
|         if (this.instance) { | ||||
|             return t`Successfully updated mapping.`; | ||||
|         } else { | ||||
|             return t`Successfully created mapping.`; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     send = (data: NotificationWebhookMapping): Promise<NotificationWebhookMapping> => { | ||||
|         if (this.instance) { | ||||
|             return new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsNotificationUpdate({ | ||||
|                 pmUuid: this.instance.pk || "", | ||||
|                 notificationWebhookMappingRequest: data, | ||||
|             }); | ||||
|         } else { | ||||
|             return new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsNotificationCreate({ | ||||
|                 notificationWebhookMappingRequest: 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="${ifDefined(this.instance?.name)}" | ||||
|                     class="pf-c-form-control" | ||||
|                     required | ||||
|                 /> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-element-horizontal label=${t`Expression`} ?required=${true} name="expression"> | ||||
|                 <ak-codemirror mode="python" value="${ifDefined(this.instance?.expression)}"> | ||||
|                 </ak-codemirror> | ||||
|                 <p class="pf-c-form__helper-text"> | ||||
|                     ${t`Expression using Python.`} | ||||
|                     <a | ||||
|                         target="_blank" | ||||
|                         href="https://goauthentik.io/docs/property-mappings/expression?utm_source=authentik" | ||||
|                     > | ||||
|                         ${t`See documentation for a list of all variables.`} | ||||
|                     </a> | ||||
|                 </p> | ||||
|             </ak-form-element-horizontal> | ||||
|         </form>`; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										93
									
								
								web/src/admin/property-mappings/PropertyMappingSAMLForm.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										93
									
								
								web/src/admin/property-mappings/PropertyMappingSAMLForm.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,93 @@ | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import "@goauthentik/elements/CodeMirror"; | ||||
| import "@goauthentik/elements/forms/HorizontalFormElement"; | ||||
| import { ModelForm } from "@goauthentik/elements/forms/ModelForm"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { TemplateResult, html } from "lit"; | ||||
| import { customElement } from "lit/decorators.js"; | ||||
| import { ifDefined } from "lit/directives/if-defined.js"; | ||||
|  | ||||
| import { PropertymappingsApi, SAMLPropertyMapping } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-property-mapping-saml-form") | ||||
| export class PropertyMappingLDAPForm extends ModelForm<SAMLPropertyMapping, string> { | ||||
|     loadInstance(pk: string): Promise<SAMLPropertyMapping> { | ||||
|         return new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsSamlRetrieve({ | ||||
|             pmUuid: pk, | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     getSuccessMessage(): string { | ||||
|         if (this.instance) { | ||||
|             return t`Successfully updated mapping.`; | ||||
|         } else { | ||||
|             return t`Successfully created mapping.`; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     send = (data: SAMLPropertyMapping): Promise<SAMLPropertyMapping> => { | ||||
|         if (this.instance) { | ||||
|             return new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsSamlUpdate({ | ||||
|                 pmUuid: this.instance.pk || "", | ||||
|                 sAMLPropertyMappingRequest: data, | ||||
|             }); | ||||
|         } else { | ||||
|             return new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsSamlCreate({ | ||||
|                 sAMLPropertyMappingRequest: 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="${ifDefined(this.instance?.name)}" | ||||
|                     class="pf-c-form-control" | ||||
|                     required | ||||
|                 /> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-element-horizontal | ||||
|                 label=${t`SAML Attribute Name`} | ||||
|                 ?required=${true} | ||||
|                 name="samlName" | ||||
|             > | ||||
|                 <input | ||||
|                     type="text" | ||||
|                     value="${ifDefined(this.instance?.samlName)}" | ||||
|                     class="pf-c-form-control" | ||||
|                     required | ||||
|                 /> | ||||
|                 <p class="pf-c-form__helper-text"> | ||||
|                     ${t`Attribute name used for SAML Assertions. Can be a URN OID, a schema reference, or a any other string. If this property mapping is used for NameID Property, this field is discarded.`} | ||||
|                 </p> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-element-horizontal label=${t`Friendly Name`} name="friendlyName"> | ||||
|                 <input | ||||
|                     type="text" | ||||
|                     value="${ifDefined(this.instance?.friendlyName || "")}" | ||||
|                     class="pf-c-form-control" | ||||
|                 /> | ||||
|                 <p class="pf-c-form__helper-text"> | ||||
|                     ${t`Optionally set the 'FriendlyName' value of the Assertion attribute.`} | ||||
|                 </p> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-element-horizontal label=${t`Expression`} ?required=${true} name="expression"> | ||||
|                 <ak-codemirror mode="python" value="${ifDefined(this.instance?.expression)}"> | ||||
|                 </ak-codemirror> | ||||
|                 <p class="pf-c-form__helper-text"> | ||||
|                     ${t`Expression using Python.`} | ||||
|                     <a | ||||
|                         target="_blank" | ||||
|                         href="https://goauthentik.io/docs/property-mappings/expression?utm_source=authentik" | ||||
|                     > | ||||
|                         ${t`See documentation for a list of all variables.`} | ||||
|                     </a> | ||||
|                 </p> | ||||
|             </ak-form-element-horizontal> | ||||
|         </form>`; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										89
									
								
								web/src/admin/property-mappings/PropertyMappingScopeForm.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								web/src/admin/property-mappings/PropertyMappingScopeForm.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,89 @@ | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import "@goauthentik/elements/CodeMirror"; | ||||
| import "@goauthentik/elements/forms/HorizontalFormElement"; | ||||
| import { ModelForm } from "@goauthentik/elements/forms/ModelForm"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { TemplateResult, html } from "lit"; | ||||
| import { customElement } from "lit/decorators.js"; | ||||
| import { ifDefined } from "lit/directives/if-defined.js"; | ||||
|  | ||||
| import { PropertymappingsApi, ScopeMapping } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-property-mapping-scope-form") | ||||
| export class PropertyMappingScopeForm extends ModelForm<ScopeMapping, string> { | ||||
|     loadInstance(pk: string): Promise<ScopeMapping> { | ||||
|         return new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsScopeRetrieve({ | ||||
|             pmUuid: pk, | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     getSuccessMessage(): string { | ||||
|         if (this.instance) { | ||||
|             return t`Successfully updated mapping.`; | ||||
|         } else { | ||||
|             return t`Successfully created mapping.`; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     send = (data: ScopeMapping): Promise<ScopeMapping> => { | ||||
|         if (this.instance) { | ||||
|             return new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsScopeUpdate({ | ||||
|                 pmUuid: this.instance.pk || "", | ||||
|                 scopeMappingRequest: data, | ||||
|             }); | ||||
|         } else { | ||||
|             return new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsScopeCreate({ | ||||
|                 scopeMappingRequest: 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="${ifDefined(this.instance?.name)}" | ||||
|                     class="pf-c-form-control" | ||||
|                     required | ||||
|                 /> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-element-horizontal label=${t`Scope name`} ?required=${true} name="scopeName"> | ||||
|                 <input | ||||
|                     type="text" | ||||
|                     value="${ifDefined(this.instance?.scopeName)}" | ||||
|                     class="pf-c-form-control" | ||||
|                     required | ||||
|                 /> | ||||
|                 <p class="pf-c-form__helper-text"> | ||||
|                     ${t`Scope which the client can specify to access these properties.`} | ||||
|                 </p> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-element-horizontal label=${t`Description`} name="description"> | ||||
|                 <input | ||||
|                     type="text" | ||||
|                     value="${ifDefined(this.instance?.description)}" | ||||
|                     class="pf-c-form-control" | ||||
|                 /> | ||||
|                 <p class="pf-c-form__helper-text"> | ||||
|                     ${t`Description shown to the user when consenting. If left empty, the user won't be informed.`} | ||||
|                 </p> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-element-horizontal label=${t`Expression`} ?required=${true} name="expression"> | ||||
|                 <ak-codemirror mode="python" value="${ifDefined(this.instance?.expression)}"> | ||||
|                 </ak-codemirror> | ||||
|                 <p class="pf-c-form__helper-text"> | ||||
|                     ${t`Expression using Python.`} | ||||
|                     <a | ||||
|                         target="_blank" | ||||
|                         href="https://goauthentik.io/docs/property-mappings/expression?utm_source=authentik" | ||||
|                     > | ||||
|                         ${t`See documentation for a list of all variables.`} | ||||
|                     </a> | ||||
|                 </p> | ||||
|             </ak-form-element-horizontal> | ||||
|         </form>`; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										99
									
								
								web/src/admin/property-mappings/PropertyMappingTestForm.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								web/src/admin/property-mappings/PropertyMappingTestForm.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,99 @@ | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { first } from "@goauthentik/common/utils"; | ||||
| import "@goauthentik/elements/CodeMirror"; | ||||
| import { Form } from "@goauthentik/elements/forms/Form"; | ||||
| import "@goauthentik/elements/forms/HorizontalFormElement"; | ||||
| import { UserOption } from "@goauthentik/elements/user/utils"; | ||||
| import YAML from "yaml"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { TemplateResult, html } from "lit"; | ||||
| import { customElement, property } from "lit/decorators.js"; | ||||
| import { ifDefined } from "lit/directives/if-defined.js"; | ||||
| import { until } from "lit/directives/until.js"; | ||||
|  | ||||
| import { | ||||
|     CoreApi, | ||||
|     PolicyTestRequest, | ||||
|     PropertyMapping, | ||||
|     PropertyMappingTestResult, | ||||
|     PropertymappingsApi, | ||||
| } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-property-mapping-test-form") | ||||
| export class PolicyTestForm extends Form<PolicyTestRequest> { | ||||
|     @property({ attribute: false }) | ||||
|     mapping?: PropertyMapping; | ||||
|  | ||||
|     @property({ attribute: false }) | ||||
|     result?: PropertyMappingTestResult; | ||||
|  | ||||
|     @property({ attribute: false }) | ||||
|     request?: PolicyTestRequest; | ||||
|  | ||||
|     getSuccessMessage(): string { | ||||
|         return t`Successfully sent test-request.`; | ||||
|     } | ||||
|  | ||||
|     send = (data: PolicyTestRequest): Promise<PropertyMappingTestResult> => { | ||||
|         this.request = data; | ||||
|         return new PropertymappingsApi(DEFAULT_CONFIG) | ||||
|             .propertymappingsAllTestCreate({ | ||||
|                 pmUuid: this.mapping?.pk || "", | ||||
|                 policyTestRequest: data, | ||||
|                 formatResult: true, | ||||
|             }) | ||||
|             .then((result) => (this.result = result)); | ||||
|     }; | ||||
|  | ||||
|     renderResult(): TemplateResult { | ||||
|         return html`<ak-form-element-horizontal label=${t`Result`}> | ||||
|             ${this.result?.successful | ||||
|                 ? html`<ak-codemirror | ||||
|                       mode="javascript" | ||||
|                       ?readOnly=${true} | ||||
|                       value="${ifDefined(this.result?.result)}" | ||||
|                   > | ||||
|                   </ak-codemirror>` | ||||
|                 : html` <div class="pf-c-form__group-label"> | ||||
|                       <div class="c-form__horizontal-group"> | ||||
|                           <span class="pf-c-form__label-text">${this.result?.result}</span> | ||||
|                       </div> | ||||
|                   </div>`} | ||||
|         </ak-form-element-horizontal>`; | ||||
|     } | ||||
|  | ||||
|     renderForm(): TemplateResult { | ||||
|         return html`<form class="pf-c-form pf-m-horizontal"> | ||||
|             <ak-form-element-horizontal label=${t`User`} ?required=${true} name="user"> | ||||
|                 <select class="pf-c-form-control"> | ||||
|                     ${until( | ||||
|                         new CoreApi(DEFAULT_CONFIG) | ||||
|                             .coreUsersList({ | ||||
|                                 ordering: "username", | ||||
|                             }) | ||||
|                             .then((users) => { | ||||
|                                 return users.results.map((user) => { | ||||
|                                     return html`<option | ||||
|                                         ?selected=${this.request?.user.toString() === | ||||
|                                         user.pk.toString()} | ||||
|                                         value=${user.pk} | ||||
|                                     > | ||||
|                                         ${UserOption(user)} | ||||
|                                     </option>`; | ||||
|                                 }); | ||||
|                             }), | ||||
|                         html`<option>${t`Loading...`}</option>`, | ||||
|                     )} | ||||
|                 </select> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-element-horizontal label=${t`Context`} name="context"> | ||||
|                 <ak-codemirror mode="yaml" value=${YAML.stringify(first(this.request?.context, {}))} | ||||
|                     >> | ||||
|                 </ak-codemirror> | ||||
|             </ak-form-element-horizontal> | ||||
|             ${this.result ? this.renderResult() : html``} | ||||
|         </form>`; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										105
									
								
								web/src/admin/property-mappings/PropertyMappingWizard.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								web/src/admin/property-mappings/PropertyMappingWizard.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,105 @@ | ||||
| import "@goauthentik/admin/property-mappings/PropertyMappingLDAPForm"; | ||||
| import "@goauthentik/admin/property-mappings/PropertyMappingNotification"; | ||||
| import "@goauthentik/admin/property-mappings/PropertyMappingSAMLForm"; | ||||
| import "@goauthentik/admin/property-mappings/PropertyMappingScopeForm"; | ||||
| import "@goauthentik/admin/property-mappings/PropertyMappingTestForm"; | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { AKElement } from "@goauthentik/elements/Base"; | ||||
| import "@goauthentik/elements/forms/ProxyForm"; | ||||
| import "@goauthentik/elements/wizard/FormWizardPage"; | ||||
| import "@goauthentik/elements/wizard/Wizard"; | ||||
| import { WizardPage } from "@goauthentik/elements/wizard/WizardPage"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { customElement } from "@lit/reactive-element/decorators/custom-element.js"; | ||||
| import { CSSResult, TemplateResult, html } from "lit"; | ||||
| import { property } from "lit/decorators.js"; | ||||
|  | ||||
| import AKGlobal from "@goauthentik/common/styles/authentik.css"; | ||||
| import PFButton from "@patternfly/patternfly/components/Button/button.css"; | ||||
| import PFForm from "@patternfly/patternfly/components/Form/form.css"; | ||||
| import PFRadio from "@patternfly/patternfly/components/Radio/radio.css"; | ||||
| import PFBase from "@patternfly/patternfly/patternfly-base.css"; | ||||
|  | ||||
| import { PropertymappingsApi, TypeCreate } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-property-mapping-wizard-initial") | ||||
| export class InitialPropertyMappingWizardPage extends WizardPage { | ||||
|     @property({ attribute: false }) | ||||
|     mappingTypes: TypeCreate[] = []; | ||||
|  | ||||
|     static get styles(): CSSResult[] { | ||||
|         return [PFBase, PFForm, PFButton, AKGlobal, PFRadio]; | ||||
|     } | ||||
|     sidebarLabel = () => t`Select type`; | ||||
|  | ||||
|     render(): TemplateResult { | ||||
|         return html`<form class="pf-c-form pf-m-horizontal"> | ||||
|             ${this.mappingTypes.map((type) => { | ||||
|                 return html`<div class="pf-c-radio"> | ||||
|                     <input | ||||
|                         class="pf-c-radio__input" | ||||
|                         type="radio" | ||||
|                         name="type" | ||||
|                         id=${`${type.component}-${type.modelName}`} | ||||
|                         @change=${() => { | ||||
|                             this.host.steps = [ | ||||
|                                 "initial", | ||||
|                                 `type-${type.component}-${type.modelName}`, | ||||
|                             ]; | ||||
|                             this.host.isValid = true; | ||||
|                         }} | ||||
|                     /> | ||||
|                     <label class="pf-c-radio__label" for=${`${type.component}-${type.modelName}`} | ||||
|                         >${type.name}</label | ||||
|                     > | ||||
|                     <span class="pf-c-radio__description">${type.description}</span> | ||||
|                 </div>`; | ||||
|             })} | ||||
|         </form>`; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @customElement("ak-property-mapping-wizard") | ||||
| export class PropertyMappingWizard extends AKElement { | ||||
|     static get styles(): CSSResult[] { | ||||
|         return [PFBase, PFButton, AKGlobal, PFRadio]; | ||||
|     } | ||||
|  | ||||
|     @property({ attribute: false }) | ||||
|     mappingTypes: TypeCreate[] = []; | ||||
|  | ||||
|     firstUpdated(): void { | ||||
|         new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsAllTypesList().then((types) => { | ||||
|             this.mappingTypes = types; | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     render(): TemplateResult { | ||||
|         return html` | ||||
|             <ak-wizard | ||||
|                 .steps=${["initial"]} | ||||
|                 header=${t`New property mapping`} | ||||
|                 description=${t`Create a new property mapping.`} | ||||
|             > | ||||
|                 <ak-property-mapping-wizard-initial | ||||
|                     slot="initial" | ||||
|                     .mappingTypes=${this.mappingTypes} | ||||
|                 > | ||||
|                 </ak-property-mapping-wizard-initial> | ||||
|                 ${this.mappingTypes.map((type) => { | ||||
|                     return html` | ||||
|                         <ak-wizard-page-form | ||||
|                             slot=${`type-${type.component}-${type.modelName}`} | ||||
|                             .sidebarLabel=${() => t`Create ${type.name}`} | ||||
|                         > | ||||
|                             <ak-proxy-form type=${type.component}></ak-proxy-form> | ||||
|                         </ak-wizard-page-form> | ||||
|                     `; | ||||
|                 })} | ||||
|                 <button slot="trigger" class="pf-c-button pf-m-primary">${t`Create`}</button> | ||||
|             </ak-wizard> | ||||
|         `; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										116
									
								
								web/src/admin/providers/ProviderListPage.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										116
									
								
								web/src/admin/providers/ProviderListPage.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,116 @@ | ||||
| import "@goauthentik/admin/providers/ProviderWizard"; | ||||
| import "@goauthentik/admin/providers/ldap/LDAPProviderForm"; | ||||
| import "@goauthentik/admin/providers/oauth2/OAuth2ProviderForm"; | ||||
| import "@goauthentik/admin/providers/proxy/ProxyProviderForm"; | ||||
| import "@goauthentik/admin/providers/saml/SAMLProviderForm"; | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { uiConfig } from "@goauthentik/common/ui/config"; | ||||
| import "@goauthentik/elements/buttons/SpinnerButton"; | ||||
| import "@goauthentik/elements/forms/DeleteBulkForm"; | ||||
| import "@goauthentik/elements/forms/ModalForm"; | ||||
| import "@goauthentik/elements/forms/ProxyForm"; | ||||
| import { PaginatedResponse } from "@goauthentik/elements/table/Table"; | ||||
| import { TableColumn } from "@goauthentik/elements/table/Table"; | ||||
| import { TablePage } from "@goauthentik/elements/table/TablePage"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { TemplateResult, html } from "lit"; | ||||
| import { customElement, property } from "lit/decorators.js"; | ||||
|  | ||||
| import { Provider, ProvidersApi } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-provider-list") | ||||
| export class ProviderListPage extends TablePage<Provider> { | ||||
|     searchEnabled(): boolean { | ||||
|         return true; | ||||
|     } | ||||
|     pageTitle(): string { | ||||
|         return t`Providers`; | ||||
|     } | ||||
|     pageDescription(): string { | ||||
|         return t`Provide support for protocols like SAML and OAuth to assigned applications.`; | ||||
|     } | ||||
|     pageIcon(): string { | ||||
|         return "pf-icon pf-icon-integration"; | ||||
|     } | ||||
|  | ||||
|     checkbox = true; | ||||
|  | ||||
|     @property() | ||||
|     order = "name"; | ||||
|  | ||||
|     async apiEndpoint(page: number): Promise<PaginatedResponse<Provider>> { | ||||
|         return new ProvidersApi(DEFAULT_CONFIG).providersAllList({ | ||||
|             ordering: this.order, | ||||
|             page: page, | ||||
|             pageSize: (await uiConfig()).pagination.perPage, | ||||
|             search: this.search || "", | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     columns(): TableColumn[] { | ||||
|         return [ | ||||
|             new TableColumn(t`Name`, "name"), | ||||
|             new TableColumn(t`Application`), | ||||
|             new TableColumn(t`Type`), | ||||
|             new TableColumn(t`Actions`), | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     renderToolbarSelected(): TemplateResult { | ||||
|         const disabled = this.selectedElements.length < 1; | ||||
|         return html`<ak-forms-delete-bulk | ||||
|             objectLabel=${t`Provider(s)`} | ||||
|             .objects=${this.selectedElements} | ||||
|             .usedBy=${(item: Provider) => { | ||||
|                 return new ProvidersApi(DEFAULT_CONFIG).providersAllUsedByList({ | ||||
|                     id: item.pk, | ||||
|                 }); | ||||
|             }} | ||||
|             .delete=${(item: Provider) => { | ||||
|                 return new ProvidersApi(DEFAULT_CONFIG).providersAllDestroy({ | ||||
|                     id: item.pk, | ||||
|                 }); | ||||
|             }} | ||||
|         > | ||||
|             <button ?disabled=${disabled} slot="trigger" class="pf-c-button pf-m-danger"> | ||||
|                 ${t`Delete`} | ||||
|             </button> | ||||
|         </ak-forms-delete-bulk>`; | ||||
|     } | ||||
|  | ||||
|     row(item: Provider): TemplateResult[] { | ||||
|         return [ | ||||
|             html`<a href="#/core/providers/${item.pk}"> ${item.name} </a>`, | ||||
|             item.assignedApplicationName | ||||
|                 ? html`<i class="pf-icon pf-icon-ok pf-m-success"></i> | ||||
|                       ${t`Assigned to application `} | ||||
|                       <a href="#/core/applications/${item.assignedApplicationSlug}" | ||||
|                           >${item.assignedApplicationName}</a | ||||
|                       >` | ||||
|                 : html`<i class="pf-icon pf-icon-warning-triangle pf-m-warning"></i> | ||||
|                       ${t`Warning: Provider not assigned to any application.`}`, | ||||
|             html`${item.verboseName}`, | ||||
|             html`<ak-forms-modal> | ||||
|                 <span slot="submit"> ${t`Update`} </span> | ||||
|                 <span slot="header"> ${t`Update ${item.verboseName}`} </span> | ||||
|                 <ak-proxy-form | ||||
|                     slot="form" | ||||
|                     .args=${{ | ||||
|                         instancePk: item.pk, | ||||
|                     }} | ||||
|                     type=${item.component} | ||||
|                 > | ||||
|                 </ak-proxy-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-provider-wizard> </ak-provider-wizard> `; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										94
									
								
								web/src/admin/providers/ProviderViewPage.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								web/src/admin/providers/ProviderViewPage.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,94 @@ | ||||
| import "@goauthentik/admin/providers/ldap/LDAPProviderViewPage"; | ||||
| import "@goauthentik/admin/providers/oauth2/OAuth2ProviderViewPage"; | ||||
| import "@goauthentik/admin/providers/proxy/ProxyProviderViewPage"; | ||||
| import "@goauthentik/admin/providers/saml/SAMLProviderViewPage"; | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { AKElement } from "@goauthentik/elements/Base"; | ||||
| import "@goauthentik/elements/EmptyState"; | ||||
| import "@goauthentik/elements/PageHeader"; | ||||
| import "@goauthentik/elements/buttons/SpinnerButton"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { CSSResult, TemplateResult, html } from "lit"; | ||||
| import { customElement, property } from "lit/decorators.js"; | ||||
| import { ifDefined } from "lit/directives/if-defined.js"; | ||||
|  | ||||
| import AKGlobal from "@goauthentik/common/styles/authentik.css"; | ||||
| import PFPage from "@patternfly/patternfly/components/Page/page.css"; | ||||
|  | ||||
| import { Provider, ProvidersApi } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-provider-view") | ||||
| export class ProviderViewPage extends AKElement { | ||||
|     @property({ type: Number }) | ||||
|     set providerID(value: number) { | ||||
|         new ProvidersApi(DEFAULT_CONFIG) | ||||
|             .providersAllRetrieve({ | ||||
|                 id: value, | ||||
|             }) | ||||
|             .then((prov) => (this.provider = prov)); | ||||
|     } | ||||
|  | ||||
|     @property({ attribute: false }) | ||||
|     provider?: Provider; | ||||
|  | ||||
|     static get styles(): CSSResult[] { | ||||
|         return [PFPage, AKGlobal]; | ||||
|     } | ||||
|  | ||||
|     renderProvider(): TemplateResult { | ||||
|         if (!this.provider) { | ||||
|             return html`<ak-empty-state ?loading=${true} ?fullHeight=${true}></ak-empty-state>`; | ||||
|         } | ||||
|         switch (this.provider?.component) { | ||||
|             case "ak-provider-saml-form": | ||||
|                 return html`<ak-provider-saml-view | ||||
|                     providerID=${ifDefined(this.provider.pk)} | ||||
|                 ></ak-provider-saml-view>`; | ||||
|             case "ak-provider-oauth2-form": | ||||
|                 return html`<ak-provider-oauth2-view | ||||
|                     providerID=${ifDefined(this.provider.pk)} | ||||
|                 ></ak-provider-oauth2-view>`; | ||||
|             case "ak-provider-proxy-form": | ||||
|                 return html`<ak-provider-proxy-view | ||||
|                     providerID=${ifDefined(this.provider.pk)} | ||||
|                 ></ak-provider-proxy-view>`; | ||||
|             case "ak-provider-ldap-form": | ||||
|                 return html`<ak-provider-ldap-view | ||||
|                     providerID=${ifDefined(this.provider.pk)} | ||||
|                 ></ak-provider-ldap-view>`; | ||||
|             default: | ||||
|                 return html`<p>Invalid provider type ${this.provider?.component}</p>`; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     render(): TemplateResult { | ||||
|         return html`<ak-page-header | ||||
|                 icon="pf-icon pf-icon-integration" | ||||
|                 header=${ifDefined(this.provider?.name)} | ||||
|                 description=${ifDefined(this.provider?.verboseName)} | ||||
|             > | ||||
|             </ak-page-header> | ||||
|             <ak-tabs> | ||||
|                 <section slot="page-overview" data-tab-title="${t`Overview`}"> | ||||
|                     ${this.renderProvider()} | ||||
|                 </section> | ||||
|                 <section | ||||
|                     slot="page-changelog" | ||||
|                     data-tab-title="${t`Changelog`}" | ||||
|                     class="pf-c-page__main-section pf-m-no-padding-mobile" | ||||
|                 > | ||||
|                     <div class="pf-c-card"> | ||||
|                         <div class="pf-c-card__body"> | ||||
|                             <ak-object-changelog | ||||
|                                 targetModelPk=${this.provider?.pk || ""} | ||||
|                                 targetModelName=${this.provider?.metaModelName || ""} | ||||
|                             > | ||||
|                             </ak-object-changelog> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                 </section> | ||||
|             </ak-tabs>`; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										129
									
								
								web/src/admin/providers/ProviderWizard.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										129
									
								
								web/src/admin/providers/ProviderWizard.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,129 @@ | ||||
| import "@goauthentik/admin/providers/ldap/LDAPProviderForm"; | ||||
| import "@goauthentik/admin/providers/oauth2/OAuth2ProviderForm"; | ||||
| import "@goauthentik/admin/providers/proxy/ProxyProviderForm"; | ||||
| import "@goauthentik/admin/providers/saml/SAMLProviderForm"; | ||||
| import "@goauthentik/admin/providers/saml/SAMLProviderImportForm"; | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { AKElement } from "@goauthentik/elements/Base"; | ||||
| import "@goauthentik/elements/forms/ProxyForm"; | ||||
| import { paramURL } from "@goauthentik/elements/router/RouterOutlet"; | ||||
| import "@goauthentik/elements/wizard/FormWizardPage"; | ||||
| import "@goauthentik/elements/wizard/Wizard"; | ||||
| import { WizardPage } from "@goauthentik/elements/wizard/WizardPage"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { customElement } from "@lit/reactive-element/decorators/custom-element.js"; | ||||
| import { CSSResult, TemplateResult, html } from "lit"; | ||||
| import { property } from "lit/decorators.js"; | ||||
|  | ||||
| import AKGlobal from "@goauthentik/common/styles/authentik.css"; | ||||
| import PFButton from "@patternfly/patternfly/components/Button/button.css"; | ||||
| import PFForm from "@patternfly/patternfly/components/Form/form.css"; | ||||
| import PFHint from "@patternfly/patternfly/components/Hint/hint.css"; | ||||
| import PFRadio from "@patternfly/patternfly/components/Radio/radio.css"; | ||||
| import PFBase from "@patternfly/patternfly/patternfly-base.css"; | ||||
|  | ||||
| import { ProvidersApi, TypeCreate } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-provider-wizard-initial") | ||||
| export class InitialProviderWizardPage extends WizardPage { | ||||
|     @property({ attribute: false }) | ||||
|     providerTypes: TypeCreate[] = []; | ||||
|  | ||||
|     static get styles(): CSSResult[] { | ||||
|         return [PFBase, PFForm, PFHint, PFButton, AKGlobal, PFRadio]; | ||||
|     } | ||||
|     sidebarLabel = () => t`Select type`; | ||||
|  | ||||
|     renderHint(): TemplateResult { | ||||
|         return html`<div class="pf-c-hint"> | ||||
|                 <div class="pf-c-hint__title">${t`Try the new application wizard`}</div> | ||||
|                 <div class="pf-c-hint__body"> | ||||
|                     ${t`The new application wizard greatly simplifies the steps required to create applications and providers.`} | ||||
|                 </div> | ||||
|                 <div class="pf-c-hint__footer"> | ||||
|                     <a | ||||
|                         class="pf-c-button pf-m-link pf-m-inline" | ||||
|                         href=${paramURL("/core/applications", { | ||||
|                             createForm: true, | ||||
|                         })} | ||||
|                         >${t`Try it now`}</a | ||||
|                     > | ||||
|                 </div> | ||||
|             </div> | ||||
|             <br />`; | ||||
|     } | ||||
|  | ||||
|     render(): TemplateResult { | ||||
|         return html` <form class="pf-c-form pf-m-horizontal"> | ||||
|             ${this.providerTypes.map((type) => { | ||||
|                 return html`<div class="pf-c-radio"> | ||||
|                     <input | ||||
|                         class="pf-c-radio__input" | ||||
|                         type="radio" | ||||
|                         name="type" | ||||
|                         id=${type.component} | ||||
|                         @change=${() => { | ||||
|                             this.host.steps = ["initial", `type-${type.component}`]; | ||||
|                             this.host.isValid = true; | ||||
|                         }} | ||||
|                     /> | ||||
|                     <label class="pf-c-radio__label" for=${type.component}>${type.name}</label> | ||||
|                     <span class="pf-c-radio__description">${type.description}</span> | ||||
|                 </div>`; | ||||
|             })} | ||||
|         </form>`; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @customElement("ak-provider-wizard") | ||||
| export class ProviderWizard extends AKElement { | ||||
|     static get styles(): CSSResult[] { | ||||
|         return [PFBase, PFButton, AKGlobal, PFRadio]; | ||||
|     } | ||||
|  | ||||
|     @property() | ||||
|     createText = t`Create`; | ||||
|  | ||||
|     @property({ attribute: false }) | ||||
|     providerTypes: TypeCreate[] = []; | ||||
|  | ||||
|     @property({ attribute: false }) | ||||
|     finalHandler: () => Promise<void> = () => { | ||||
|         return Promise.resolve(); | ||||
|     }; | ||||
|  | ||||
|     firstUpdated(): void { | ||||
|         new ProvidersApi(DEFAULT_CONFIG).providersAllTypesList().then((types) => { | ||||
|             this.providerTypes = types; | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     render(): TemplateResult { | ||||
|         return html` | ||||
|             <ak-wizard | ||||
|                 .steps=${["initial"]} | ||||
|                 header=${t`New provider`} | ||||
|                 description=${t`Create a new provider.`} | ||||
|                 .finalHandler=${() => { | ||||
|                     return this.finalHandler(); | ||||
|                 }} | ||||
|             > | ||||
|                 <ak-provider-wizard-initial slot="initial" .providerTypes=${this.providerTypes}> | ||||
|                 </ak-provider-wizard-initial> | ||||
|                 ${this.providerTypes.map((type) => { | ||||
|                     return html` | ||||
|                         <ak-wizard-page-form | ||||
|                             slot=${`type-${type.component}`} | ||||
|                             .sidebarLabel=${() => t`Create ${type.name}`} | ||||
|                         > | ||||
|                             <ak-proxy-form type=${type.component}></ak-proxy-form> | ||||
|                         </ak-wizard-page-form> | ||||
|                     `; | ||||
|                 })} | ||||
|                 <button slot="trigger" class="pf-c-button pf-m-primary">${this.createText}</button> | ||||
|             </ak-wizard> | ||||
|         `; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										38
									
								
								web/src/admin/providers/RelatedApplicationButton.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								web/src/admin/providers/RelatedApplicationButton.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,38 @@ | ||||
| import "@goauthentik/admin/applications/ApplicationForm"; | ||||
| import { AKElement } from "@goauthentik/elements/Base"; | ||||
| import "@goauthentik/elements/Spinner"; | ||||
| import "@goauthentik/elements/forms/ModalForm"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { CSSResult, TemplateResult, html } from "lit"; | ||||
| import { customElement, property } from "lit/decorators.js"; | ||||
|  | ||||
| import PFButton from "@patternfly/patternfly/components/Button/button.css"; | ||||
| import PFBase from "@patternfly/patternfly/patternfly-base.css"; | ||||
|  | ||||
| import { Provider } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-provider-related-application") | ||||
| export class RelatedApplicationButton extends AKElement { | ||||
|     static get styles(): CSSResult[] { | ||||
|         return [PFBase, PFButton]; | ||||
|     } | ||||
|  | ||||
|     @property({ attribute: false }) | ||||
|     provider?: Provider; | ||||
|  | ||||
|     render(): TemplateResult { | ||||
|         if (this.provider?.assignedApplicationSlug) { | ||||
|             return html`<a href="#/core/applications/${this.provider.assignedApplicationSlug}"> | ||||
|                 ${this.provider.assignedApplicationName} | ||||
|             </a>`; | ||||
|         } | ||||
|         return html`<ak-forms-modal> | ||||
|             <span slot="submit"> ${t`Create`} </span> | ||||
|             <span slot="header"> ${t`Create Application`} </span> | ||||
|             <ak-application-form slot="form" .provider=${this.provider?.pk}> </ak-application-form> | ||||
|             <button slot="trigger" class="pf-c-button pf-m-primary">${t`Create`}</button> | ||||
|         </ak-forms-modal>`; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										245
									
								
								web/src/admin/providers/ldap/LDAPProviderForm.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										245
									
								
								web/src/admin/providers/ldap/LDAPProviderForm.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,245 @@ | ||||
| import { DEFAULT_CONFIG, tenant } from "@goauthentik/common/api/config"; | ||||
| import { first } from "@goauthentik/common/utils"; | ||||
| import "@goauthentik/elements/forms/FormGroup"; | ||||
| import "@goauthentik/elements/forms/HorizontalFormElement"; | ||||
| import { ModelForm } from "@goauthentik/elements/forms/ModelForm"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { TemplateResult, html } from "lit"; | ||||
| import { customElement } from "lit/decorators.js"; | ||||
| import { ifDefined } from "lit/directives/if-defined.js"; | ||||
| import { until } from "lit/directives/until.js"; | ||||
|  | ||||
| import { | ||||
|     CoreApi, | ||||
|     CryptoApi, | ||||
|     FlowsApi, | ||||
|     FlowsInstancesListDesignationEnum, | ||||
|     LDAPAPIAccessMode, | ||||
|     LDAPProvider, | ||||
|     ProvidersApi, | ||||
| } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-provider-ldap-form") | ||||
| export class LDAPProviderFormPage extends ModelForm<LDAPProvider, number> { | ||||
|     loadInstance(pk: number): Promise<LDAPProvider> { | ||||
|         return new ProvidersApi(DEFAULT_CONFIG).providersLdapRetrieve({ | ||||
|             id: pk, | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     getSuccessMessage(): string { | ||||
|         if (this.instance) { | ||||
|             return t`Successfully updated provider.`; | ||||
|         } else { | ||||
|             return t`Successfully created provider.`; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     send = (data: LDAPProvider): Promise<LDAPProvider> => { | ||||
|         if (this.instance) { | ||||
|             return new ProvidersApi(DEFAULT_CONFIG).providersLdapUpdate({ | ||||
|                 id: this.instance.pk || 0, | ||||
|                 lDAPProviderRequest: data, | ||||
|             }); | ||||
|         } else { | ||||
|             data.tlsServerName = ""; | ||||
|             return new ProvidersApi(DEFAULT_CONFIG).providersLdapCreate({ | ||||
|                 lDAPProviderRequest: 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="${ifDefined(this.instance?.name)}" | ||||
|                     class="pf-c-form-control" | ||||
|                     required | ||||
|                 /> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-element-horizontal | ||||
|                 label=${t`Bind flow`} | ||||
|                 ?required=${true} | ||||
|                 name="authorizationFlow" | ||||
|             > | ||||
|                 <select class="pf-c-form-control"> | ||||
|                     ${until( | ||||
|                         tenant().then((t) => { | ||||
|                             return new FlowsApi(DEFAULT_CONFIG) | ||||
|                                 .flowsInstancesList({ | ||||
|                                     ordering: "slug", | ||||
|                                     designation: FlowsInstancesListDesignationEnum.Authentication, | ||||
|                                 }) | ||||
|                                 .then((flows) => { | ||||
|                                     return flows.results.map((flow) => { | ||||
|                                         let selected = flow.pk === t.flowAuthentication; | ||||
|                                         if (this.instance?.authorizationFlow === flow.pk) { | ||||
|                                             selected = true; | ||||
|                                         } | ||||
|                                         return html`<option | ||||
|                                             value=${ifDefined(flow.pk)} | ||||
|                                             ?selected=${selected} | ||||
|                                         > | ||||
|                                             ${flow.name} (${flow.slug}) | ||||
|                                         </option>`; | ||||
|                                     }); | ||||
|                                 }); | ||||
|                         }), | ||||
|                         html`<option>${t`Loading...`}</option>`, | ||||
|                     )} | ||||
|                 </select> | ||||
|                 <p class="pf-c-form__helper-text"> | ||||
|                     ${t`Flow used for users to authenticate. Currently only identification and password stages are supported.`} | ||||
|                 </p> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-element-horizontal label=${t`Search group`} name="searchGroup"> | ||||
|                 <select class="pf-c-form-control"> | ||||
|                     <option value="" ?selected=${this.instance?.searchGroup === undefined}> | ||||
|                         --------- | ||||
|                     </option> | ||||
|                     ${until( | ||||
|                         new CoreApi(DEFAULT_CONFIG).coreGroupsList({}).then((groups) => { | ||||
|                             return groups.results.map((group) => { | ||||
|                                 return html`<option | ||||
|                                     value=${ifDefined(group.pk)} | ||||
|                                     ?selected=${this.instance?.searchGroup === group.pk} | ||||
|                                 > | ||||
|                                     ${group.name} | ||||
|                                 </option>`; | ||||
|                             }); | ||||
|                         }), | ||||
|                         html`<option>${t`Loading...`}</option>`, | ||||
|                     )} | ||||
|                 </select> | ||||
|                 <p class="pf-c-form__helper-text"> | ||||
|                     ${t`Users in the selected group can do search queries. If no group is selected, no LDAP Searches are allowed.`} | ||||
|                 </p> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-element-horizontal label=${t`Bind mode`} name="bindMode"> | ||||
|                 <select class="pf-c-form-control"> | ||||
|                     <option | ||||
|                         value="${LDAPAPIAccessMode.Cached}" | ||||
|                         ?selected=${this.instance?.bindMode === LDAPAPIAccessMode.Cached} | ||||
|                     > | ||||
|                         ${t`Cached binding, flow is executed and session is cached in memory. Flow is executed when session expires.`} | ||||
|                     </option> | ||||
|                     <option | ||||
|                         value="${LDAPAPIAccessMode.Direct}" | ||||
|                         ?selected=${this.instance?.bindMode === LDAPAPIAccessMode.Direct} | ||||
|                     > | ||||
|                         ${t`Direct binding, always execute the configured bind flow to authenticate the user.`} | ||||
|                     </option> | ||||
|                 </select> | ||||
|                 <p class="pf-c-form__helper-text"> | ||||
|                     ${t`Configure how the outpost authenticates requests.`} | ||||
|                 </p> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-element-horizontal label=${t`Search mode`} name="searchMode"> | ||||
|                 <select class="pf-c-form-control"> | ||||
|                     <option | ||||
|                         value="${LDAPAPIAccessMode.Cached}" | ||||
|                         ?selected=${this.instance?.searchMode === LDAPAPIAccessMode.Cached} | ||||
|                     > | ||||
|                         ${t`Cached querying, the outpost holds all users and groups in-memory and will refresh every 5 Minutes.`} | ||||
|                     </option> | ||||
|                     <option | ||||
|                         value="${LDAPAPIAccessMode.Direct}" | ||||
|                         ?selected=${this.instance?.searchMode === LDAPAPIAccessMode.Direct} | ||||
|                     > | ||||
|                         ${t`Direct querying, always returns the latest data, but slower than cached querying.`} | ||||
|                     </option> | ||||
|                 </select> | ||||
|                 <p class="pf-c-form__helper-text"> | ||||
|                     ${t`Configure how the outpost queries the core authentik server's users.`} | ||||
|                 </p> | ||||
|             </ak-form-element-horizontal> | ||||
|  | ||||
|             <ak-form-group .expanded=${true}> | ||||
|                 <span slot="header"> ${t`Protocol settings`} </span> | ||||
|                 <div slot="body" class="pf-c-form"> | ||||
|                     <ak-form-element-horizontal label=${t`Base DN`} ?required=${true} name="baseDn"> | ||||
|                         <input | ||||
|                             type="text" | ||||
|                             value="${first(this.instance?.baseDn, "DC=ldap,DC=goauthentik,DC=io")}" | ||||
|                             class="pf-c-form-control" | ||||
|                             required | ||||
|                         /> | ||||
|                         <p class="pf-c-form__helper-text"> | ||||
|                             ${t`LDAP DN under which bind requests and search requests can be made.`} | ||||
|                         </p> | ||||
|                     </ak-form-element-horizontal> | ||||
|                     <ak-form-element-horizontal label=${t`Certificate`} name="certificate"> | ||||
|                         <select class="pf-c-form-control"> | ||||
|                             <option value="" ?selected=${this.instance?.certificate === undefined}> | ||||
|                                 --------- | ||||
|                             </option> | ||||
|                             ${until( | ||||
|                                 new CryptoApi(DEFAULT_CONFIG) | ||||
|                                     .cryptoCertificatekeypairsList({ | ||||
|                                         ordering: "name", | ||||
|                                         hasKey: true, | ||||
|                                     }) | ||||
|                                     .then((keys) => { | ||||
|                                         return keys.results.map((key) => { | ||||
|                                             return html`<option | ||||
|                                                 value=${ifDefined(key.pk)} | ||||
|                                                 ?selected=${this.instance?.certificate === key.pk} | ||||
|                                             > | ||||
|                                                 ${key.name} | ||||
|                                             </option>`; | ||||
|                                         }); | ||||
|                                     }), | ||||
|                                 html`<option | ||||
|                                     value=${ifDefined(this.instance?.certificate || undefined)} | ||||
|                                     ?selected=${this.instance?.certificate !== undefined} | ||||
|                                 > | ||||
|                                     ${t`Loading...`} | ||||
|                                 </option>`, | ||||
|                             )} | ||||
|                         </select> | ||||
|                         <p class="pf-c-form__helper-text"> | ||||
|                             ${t`Due to protocol limitations, this certificate is only used when the outpost has a single provider.`} | ||||
|                         </p> | ||||
|                         <p class="pf-c-form__helper-text"> | ||||
|                             ${t`If multiple providers share an outpost, a self-signed certificate is used.`} | ||||
|                         </p> | ||||
|                     </ak-form-element-horizontal> | ||||
|                     <ak-form-element-horizontal | ||||
|                         label=${t`UID start number`} | ||||
|                         ?required=${true} | ||||
|                         name="uidStartNumber" | ||||
|                     > | ||||
|                         <input | ||||
|                             type="number" | ||||
|                             value="${first(this.instance?.uidStartNumber, 2000)}" | ||||
|                             class="pf-c-form-control" | ||||
|                             required | ||||
|                         /> | ||||
|                         <p class="pf-c-form__helper-text"> | ||||
|                             ${t`The start for uidNumbers, this number is added to the user.Pk to make sure that the numbers aren't too low for POSIX users. Default is 2000 to ensure that we don't collide with local users uidNumber`} | ||||
|                         </p> | ||||
|                     </ak-form-element-horizontal> | ||||
|                     <ak-form-element-horizontal | ||||
|                         label=${t`GID start number`} | ||||
|                         ?required=${true} | ||||
|                         name="gidStartNumber" | ||||
|                     > | ||||
|                         <input | ||||
|                             type="number" | ||||
|                             value="${first(this.instance?.gidStartNumber, 4000)}" | ||||
|                             class="pf-c-form-control" | ||||
|                             required | ||||
|                         /> | ||||
|                         <p class="pf-c-form__helper-text"> | ||||
|                             ${t`The start for gidNumbers, this number is added to a number generated from the group.Pk to make sure that the numbers aren't too low for POSIX groups. Default is 4000 to ensure that we don't collide with local groups or users primary groups gidNumber`} | ||||
|                         </p> | ||||
|                     </ak-form-element-horizontal> | ||||
|                 </div> | ||||
|             </ak-form-group> | ||||
|         </form>`; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										210
									
								
								web/src/admin/providers/ldap/LDAPProviderViewPage.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										210
									
								
								web/src/admin/providers/ldap/LDAPProviderViewPage.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,210 @@ | ||||
| import "@goauthentik/admin/providers/RelatedApplicationButton"; | ||||
| import "@goauthentik/admin/providers/ldap/LDAPProviderForm"; | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { EVENT_REFRESH } from "@goauthentik/common/constants"; | ||||
| import { me } from "@goauthentik/common/users"; | ||||
| import { AKElement } from "@goauthentik/elements/Base"; | ||||
| import "@goauthentik/elements/CodeMirror"; | ||||
| import "@goauthentik/elements/Tabs"; | ||||
| import "@goauthentik/elements/buttons/ModalButton"; | ||||
| import "@goauthentik/elements/buttons/SpinnerButton"; | ||||
| import "@goauthentik/elements/events/ObjectChangelog"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { CSSResult, TemplateResult, html } from "lit"; | ||||
| import { until } from "lit-html/directives/until.js"; | ||||
| import { customElement, property } from "lit/decorators.js"; | ||||
| import { ifDefined } from "lit/directives/if-defined.js"; | ||||
|  | ||||
| import AKGlobal from "@goauthentik/common/styles/authentik.css"; | ||||
| import PFBanner from "@patternfly/patternfly/components/Banner/banner.css"; | ||||
| import PFButton from "@patternfly/patternfly/components/Button/button.css"; | ||||
| import PFCard from "@patternfly/patternfly/components/Card/card.css"; | ||||
| import PFContent from "@patternfly/patternfly/components/Content/content.css"; | ||||
| import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css"; | ||||
| import PFForm from "@patternfly/patternfly/components/Form/form.css"; | ||||
| import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css"; | ||||
| import PFList from "@patternfly/patternfly/components/List/list.css"; | ||||
| import PFPage from "@patternfly/patternfly/components/Page/page.css"; | ||||
| import PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css"; | ||||
| import PFBase from "@patternfly/patternfly/patternfly-base.css"; | ||||
|  | ||||
| import { LDAPProvider, ProvidersApi } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-provider-ldap-view") | ||||
| export class LDAPProviderViewPage extends AKElement { | ||||
|     @property() | ||||
|     set args(value: { [key: string]: number }) { | ||||
|         this.providerID = value.id; | ||||
|     } | ||||
|  | ||||
|     @property({ type: Number }) | ||||
|     set providerID(value: number) { | ||||
|         new ProvidersApi(DEFAULT_CONFIG) | ||||
|             .providersLdapRetrieve({ | ||||
|                 id: value, | ||||
|             }) | ||||
|             .then((prov) => (this.provider = prov)); | ||||
|     } | ||||
|  | ||||
|     @property({ attribute: false }) | ||||
|     provider?: LDAPProvider; | ||||
|  | ||||
|     static get styles(): CSSResult[] { | ||||
|         return [ | ||||
|             PFBase, | ||||
|             PFButton, | ||||
|             PFBanner, | ||||
|             PFForm, | ||||
|             PFFormControl, | ||||
|             PFList, | ||||
|             PFGrid, | ||||
|             PFPage, | ||||
|             PFContent, | ||||
|             PFCard, | ||||
|             PFDescriptionList, | ||||
|             AKGlobal, | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     constructor() { | ||||
|         super(); | ||||
|         this.addEventListener(EVENT_REFRESH, () => { | ||||
|             if (!this.provider?.pk) return; | ||||
|             this.providerID = this.provider?.pk; | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     render(): TemplateResult { | ||||
|         if (!this.provider) { | ||||
|             return html``; | ||||
|         } | ||||
|         return html`${ | ||||
|             this.provider?.assignedApplicationName | ||||
|                 ? html`` | ||||
|                 : html`<div slot="header" class="pf-c-banner pf-m-warning"> | ||||
|                       ${t`Warning: Provider is not used by an Application.`} | ||||
|                   </div>` | ||||
|         } | ||||
|             ${ | ||||
|                 this.provider?.outpostSet.length < 1 | ||||
|                     ? html`<div slot="header" class="pf-c-banner pf-m-warning"> | ||||
|                           ${t`Warning: Provider is not used by any Outpost.`} | ||||
|                       </div>` | ||||
|                     : html`` | ||||
|             } | ||||
|             <div class="pf-c-page__main-section pf-m-no-padding-mobile pf-l-grid pf-m-gutter"> | ||||
|                 <div class="pf-c-card pf-l-grid__item pf-m-12-col"> | ||||
|                     <div class="pf-c-card__body"> | ||||
|                         <dl class="pf-c-description-list pf-m-3-col-on-lg"> | ||||
|                             <div class="pf-c-description-list__group"> | ||||
|                                 <dt class="pf-c-description-list__term"> | ||||
|                                     <span class="pf-c-description-list__text">${t`Name`}</span> | ||||
|                                 </dt> | ||||
|                                 <dd class="pf-c-description-list__description"> | ||||
|                                     <div class="pf-c-description-list__text"> | ||||
|                                         ${this.provider.name} | ||||
|                                     </div> | ||||
|                                 </dd> | ||||
|                             </div> | ||||
|                             <div class="pf-c-description-list__group"> | ||||
|                                 <dt class="pf-c-description-list__term"> | ||||
|                                     <span class="pf-c-description-list__text" | ||||
|                                         >${t`Assigned to application`}</span | ||||
|                                     > | ||||
|                                 </dt> | ||||
|                                 <dd class="pf-c-description-list__description"> | ||||
|                                     <div class="pf-c-description-list__text"> | ||||
|                                         <ak-provider-related-application | ||||
|                                             .provider=${this.provider} | ||||
|                                         ></ak-provider-related-application> | ||||
|                                     </div> | ||||
|                                 </dd> | ||||
|                             </div> | ||||
|                             <div class="pf-c-description-list__group"> | ||||
|                                 <dt class="pf-c-description-list__term"> | ||||
|                                     <span class="pf-c-description-list__text" | ||||
|                                         >${t`Base DN`}</span | ||||
|                                     > | ||||
|                                 </dt> | ||||
|                                 <dd class="pf-c-description-list__description"> | ||||
|                                     <div class="pf-c-description-list__text"> | ||||
|                                         ${this.provider.baseDn} | ||||
|                                     </div> | ||||
|                                 </dd> | ||||
|                             </div> | ||||
|                         </dl> | ||||
|                     </div> | ||||
|                     <div class="pf-c-card__footer"> | ||||
|                         <ak-forms-modal> | ||||
|                             <span slot="submit"> ${t`Update`} </span> | ||||
|                             <span slot="header"> ${t`Update LDAP Provider`} </span> | ||||
|                             <ak-provider-ldap-form slot="form" .instancePk=${this.provider.pk}> | ||||
|                             </ak-provider-ldap-form> | ||||
|                             <button slot="trigger" class="pf-c-button pf-m-primary"> | ||||
|                                 ${t`Edit`} | ||||
|                             </button> | ||||
|                         </ak-forms-modal> | ||||
|                     </div> | ||||
|                 </div> | ||||
|                 <div class="pf-c-card pf-l-grid__item pf-m-12-col"> | ||||
|                     <div class="pf-c-card__title"> | ||||
|                         ${t`How to connect`} | ||||
|                     </div> | ||||
|                     <div class="pf-c-card__body"> | ||||
|                         <p> | ||||
|                             ${t`Connect to the LDAP Server on port 389:`} | ||||
|                         </p> | ||||
|                         <ul class="pf-c-list"> | ||||
|                             <li>${t`Check the IP of the Kubernetes service, or`}</li> | ||||
|                             <li>${t`The Host IP of the docker host`}</li> | ||||
|                         </ul> | ||||
|                         <form class="pf-c-form"> | ||||
|                             <div class="pf-c-form__group"> | ||||
|                                 <label class="pf-c-form__label"> | ||||
|                                     <span class="pf-c-form__label-text">${t`Bind DN`}</span> | ||||
|                                 </label> | ||||
|                                 <!-- @ts-ignore --> | ||||
|                                 <input | ||||
|                                     class="pf-c-form-control" | ||||
|                                     readonly | ||||
|                                     type="text" | ||||
|                                     value=${until( | ||||
|                                         me().then((m) => { | ||||
|                                             return `cn=${ | ||||
|                                                 m.user.username | ||||
|                                             },ou=users,${this.provider?.baseDn?.toLowerCase()}`; | ||||
|                                         }), | ||||
|                                     )} | ||||
|                                 /> | ||||
|                             </div> | ||||
|                             <div class="pf-c-form__group"> | ||||
|                                 <label class="pf-c-form__label"> | ||||
|                                     <span class="pf-c-form__label-text">${t`Bind Password`}</span> | ||||
|                                 </label> | ||||
|                                 <input | ||||
|                                     class="pf-c-form-control" | ||||
|                                     readonly | ||||
|                                     type="text" | ||||
|                                     value="Your authentik password" | ||||
|                                 /> | ||||
|                             </div> | ||||
|                             <div class="pf-c-form__group"> | ||||
|                                 <label class="pf-c-form__label"> | ||||
|                                     <span class="pf-c-form__label-text">${t`Search base`}</span> | ||||
|                                 </label> | ||||
|                                 <input | ||||
|                                     class="pf-c-form-control" | ||||
|                                     readonly | ||||
|                                     type="text" | ||||
|                                     value=${ifDefined(this.provider?.baseDn?.toLowerCase())} | ||||
|                                 /> | ||||
|                             </div> | ||||
|                         </form> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </div> | ||||
|         </div>`; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										417
									
								
								web/src/admin/providers/oauth2/OAuth2ProviderForm.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										417
									
								
								web/src/admin/providers/oauth2/OAuth2ProviderForm.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,417 @@ | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { first, randomString } from "@goauthentik/common/utils"; | ||||
| import "@goauthentik/elements/forms/FormGroup"; | ||||
| import "@goauthentik/elements/forms/HorizontalFormElement"; | ||||
| import { ModelForm } from "@goauthentik/elements/forms/ModelForm"; | ||||
| import "@goauthentik/elements/utils/TimeDeltaHelp"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { TemplateResult, html } from "lit"; | ||||
| import { customElement, property } from "lit/decorators.js"; | ||||
| import { ifDefined } from "lit/directives/if-defined.js"; | ||||
| import { until } from "lit/directives/until.js"; | ||||
|  | ||||
| import { | ||||
|     ClientTypeEnum, | ||||
|     CryptoApi, | ||||
|     FlowsApi, | ||||
|     FlowsInstancesListDesignationEnum, | ||||
|     IssuerModeEnum, | ||||
|     OAuth2Provider, | ||||
|     PropertymappingsApi, | ||||
|     ProvidersApi, | ||||
|     SourcesApi, | ||||
|     SubModeEnum, | ||||
| } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-provider-oauth2-form") | ||||
| export class OAuth2ProviderFormPage extends ModelForm<OAuth2Provider, number> { | ||||
|     loadInstance(pk: number): Promise<OAuth2Provider> { | ||||
|         return new ProvidersApi(DEFAULT_CONFIG) | ||||
|             .providersOauth2Retrieve({ | ||||
|                 id: pk, | ||||
|             }) | ||||
|             .then((provider) => { | ||||
|                 this.showClientSecret = provider.clientType === ClientTypeEnum.Confidential; | ||||
|                 return provider; | ||||
|             }); | ||||
|     } | ||||
|  | ||||
|     @property({ type: Boolean }) | ||||
|     showClientSecret = true; | ||||
|  | ||||
|     getSuccessMessage(): string { | ||||
|         if (this.instance) { | ||||
|             return t`Successfully updated provider.`; | ||||
|         } else { | ||||
|             return t`Successfully created provider.`; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     send = (data: OAuth2Provider): Promise<OAuth2Provider> => { | ||||
|         if (this.instance) { | ||||
|             return new ProvidersApi(DEFAULT_CONFIG).providersOauth2Update({ | ||||
|                 id: this.instance.pk || 0, | ||||
|                 oAuth2ProviderRequest: data, | ||||
|             }); | ||||
|         } else { | ||||
|             return new ProvidersApi(DEFAULT_CONFIG).providersOauth2Create({ | ||||
|                 oAuth2ProviderRequest: 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="${ifDefined(this.instance?.name)}" | ||||
|                     class="pf-c-form-control" | ||||
|                     required | ||||
|                 /> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-element-horizontal | ||||
|                 label=${t`Authorization flow`} | ||||
|                 ?required=${true} | ||||
|                 name="authorizationFlow" | ||||
|             > | ||||
|                 <select class="pf-c-form-control"> | ||||
|                     ${until( | ||||
|                         new FlowsApi(DEFAULT_CONFIG) | ||||
|                             .flowsInstancesList({ | ||||
|                                 ordering: "slug", | ||||
|                                 designation: FlowsInstancesListDesignationEnum.Authorization, | ||||
|                             }) | ||||
|                             .then((flows) => { | ||||
|                                 return flows.results.map((flow) => { | ||||
|                                     return html`<option | ||||
|                                         value=${ifDefined(flow.pk)} | ||||
|                                         ?selected=${this.instance?.authorizationFlow === flow.pk} | ||||
|                                     > | ||||
|                                         ${flow.name} (${flow.slug}) | ||||
|                                     </option>`; | ||||
|                                 }); | ||||
|                             }), | ||||
|                         html`<option>${t`Loading...`}</option>`, | ||||
|                     )} | ||||
|                 </select> | ||||
|                 <p class="pf-c-form__helper-text"> | ||||
|                     ${t`Flow used when authorizing this provider.`} | ||||
|                 </p> | ||||
|             </ak-form-element-horizontal> | ||||
|  | ||||
|             <ak-form-group .expanded=${true}> | ||||
|                 <span slot="header"> ${t`Protocol settings`} </span> | ||||
|                 <div slot="body" class="pf-c-form"> | ||||
|                     <ak-form-element-horizontal | ||||
|                         label=${t`Client type`} | ||||
|                         ?required=${true} | ||||
|                         name="clientType" | ||||
|                     > | ||||
|                         <select | ||||
|                             class="pf-c-form-control" | ||||
|                             @change=${(ev: Event) => { | ||||
|                                 const target = ev.target as HTMLSelectElement; | ||||
|                                 if (target.selectedOptions[0].value === ClientTypeEnum.Public) { | ||||
|                                     this.showClientSecret = false; | ||||
|                                 } else { | ||||
|                                     this.showClientSecret = true; | ||||
|                                 } | ||||
|                             }} | ||||
|                         > | ||||
|                             <option | ||||
|                                 value=${ClientTypeEnum.Confidential} | ||||
|                                 ?selected=${this.instance?.clientType === | ||||
|                                 ClientTypeEnum.Confidential} | ||||
|                             > | ||||
|                                 ${t`Confidential`} | ||||
|                             </option> | ||||
|                             <option | ||||
|                                 value=${ClientTypeEnum.Public} | ||||
|                                 ?selected=${this.instance?.clientType === ClientTypeEnum.Public} | ||||
|                             > | ||||
|                                 ${t`Public`} | ||||
|                             </option> | ||||
|                         </select> | ||||
|                         <p class="pf-c-form__helper-text"> | ||||
|                             ${t`Confidential clients are capable of maintaining the confidentiality of their credentials. Public clients are incapable.`} | ||||
|                         </p> | ||||
|                     </ak-form-element-horizontal> | ||||
|                     <ak-form-element-horizontal | ||||
|                         label=${t`Client ID`} | ||||
|                         ?required=${true} | ||||
|                         name="clientId" | ||||
|                     > | ||||
|                         <input | ||||
|                             type="text" | ||||
|                             value="${first(this.instance?.clientId, randomString(40))}" | ||||
|                             class="pf-c-form-control" | ||||
|                             required | ||||
|                         /> | ||||
|                     </ak-form-element-horizontal> | ||||
|                     <ak-form-element-horizontal | ||||
|                         ?hidden=${!this.showClientSecret} | ||||
|                         label=${t`Client Secret`} | ||||
|                         name="clientSecret" | ||||
|                     > | ||||
|                         <input | ||||
|                             type="text" | ||||
|                             value="${first(this.instance?.clientSecret, randomString(128))}" | ||||
|                             class="pf-c-form-control" | ||||
|                         /> | ||||
|                     </ak-form-element-horizontal> | ||||
|                     <ak-form-element-horizontal | ||||
|                         label=${t`Redirect URIs/Origins (RegEx)`} | ||||
|                         name="redirectUris" | ||||
|                     > | ||||
|                         <textarea class="pf-c-form-control"> | ||||
| ${this.instance?.redirectUris}</textarea | ||||
|                         > | ||||
|                         <p class="pf-c-form__helper-text"> | ||||
|                             ${t`Valid redirect URLs after a successful authorization flow. Also specify any origins here for Implicit flows.`} | ||||
|                         </p> | ||||
|                         <p class="pf-c-form__helper-text"> | ||||
|                             ${t`If no explicit redirect URIs are specified, the first successfully used redirect URI will be saved.`} | ||||
|                         </p> | ||||
|                         <p class="pf-c-form__helper-text"> | ||||
|                             ${t`To allow any redirect URI, set this value to ".*". Be aware of the possible security implications this can have.`} | ||||
|                         </p> | ||||
|                     </ak-form-element-horizontal> | ||||
|                     <ak-form-element-horizontal label=${t`Signing Key`} name="signingKey"> | ||||
|                         <select class="pf-c-form-control"> | ||||
|                             <option value="" ?selected=${this.instance?.signingKey === undefined}> | ||||
|                                 --------- | ||||
|                             </option> | ||||
|                             ${until( | ||||
|                                 new CryptoApi(DEFAULT_CONFIG) | ||||
|                                     .cryptoCertificatekeypairsList({ | ||||
|                                         ordering: "name", | ||||
|                                         hasKey: true, | ||||
|                                     }) | ||||
|                                     .then((keys) => { | ||||
|                                         return keys.results.map((key) => { | ||||
|                                             let selected = this.instance?.signingKey === key.pk; | ||||
|                                             if (!this.instance && keys.results.length === 1) { | ||||
|                                                 selected = true; | ||||
|                                             } | ||||
|                                             return html`<option | ||||
|                                                 value=${ifDefined(key.pk)} | ||||
|                                                 ?selected=${selected} | ||||
|                                             > | ||||
|                                                 ${key.name} (${key.privateKeyType?.toUpperCase()}) | ||||
|                                             </option>`; | ||||
|                                         }); | ||||
|                                     }), | ||||
|                                 html`<option | ||||
|                                     value=${ifDefined(this.instance?.signingKey || undefined)} | ||||
|                                     ?selected=${this.instance?.signingKey !== undefined} | ||||
|                                 > | ||||
|                                     ${t`Loading...`} | ||||
|                                 </option>`, | ||||
|                             )} | ||||
|                         </select> | ||||
|                         <p class="pf-c-form__helper-text">${t`Key used to sign the tokens.`}</p> | ||||
|                     </ak-form-element-horizontal> | ||||
|                 </div> | ||||
|             </ak-form-group> | ||||
|  | ||||
|             <ak-form-group> | ||||
|                 <span slot="header"> ${t`Advanced protocol settings`} </span> | ||||
|                 <div slot="body" class="pf-c-form"> | ||||
|                     <ak-form-element-horizontal | ||||
|                         label=${t`Access token validity`} | ||||
|                         ?required=${true} | ||||
|                         name="accessCodeValidity" | ||||
|                     > | ||||
|                         <input | ||||
|                             type="text" | ||||
|                             value="${first(this.instance?.accessCodeValidity, "minutes=1")}" | ||||
|                             class="pf-c-form-control" | ||||
|                             required | ||||
|                         /> | ||||
|                         <p class="pf-c-form__helper-text"> | ||||
|                             ${t`Configure how long access tokens are valid for.`} | ||||
|                         </p> | ||||
|                         <p class="pf-c-form__helper-text"> | ||||
|                             ${t`If you are using an Implicit, client-side flow (where the token-endpoint isn't used), you probably want to increase this time.`} | ||||
|                         </p> | ||||
|                         <ak-utils-time-delta-help></ak-utils-time-delta-help> | ||||
|                     </ak-form-element-horizontal> | ||||
|                     <ak-form-element-horizontal | ||||
|                         label=${t`Token validity`} | ||||
|                         ?required=${true} | ||||
|                         name="tokenValidity" | ||||
|                     > | ||||
|                         <input | ||||
|                             type="text" | ||||
|                             value="${first(this.instance?.tokenValidity, "days=30")}" | ||||
|                             class="pf-c-form-control" | ||||
|                             required | ||||
|                         /> | ||||
|                         <p class="pf-c-form__helper-text"> | ||||
|                             ${t`Configure how long refresh tokens and their id_tokens are valid for.`} | ||||
|                         </p> | ||||
|                         <ak-utils-time-delta-help></ak-utils-time-delta-help> | ||||
|                     </ak-form-element-horizontal> | ||||
|                     <ak-form-element-horizontal label=${t`Scopes`} name="propertyMappings"> | ||||
|                         <select class="pf-c-form-control" multiple> | ||||
|                             ${until( | ||||
|                                 new PropertymappingsApi(DEFAULT_CONFIG) | ||||
|                                     .propertymappingsScopeList({ | ||||
|                                         ordering: "scope_name", | ||||
|                                     }) | ||||
|                                     .then((scopes) => { | ||||
|                                         return scopes.results.map((scope) => { | ||||
|                                             let selected = false; | ||||
|                                             if (!this.instance?.propertyMappings) { | ||||
|                                                 selected = | ||||
|                                                     scope.managed?.startsWith( | ||||
|                                                         "goauthentik.io/providers/oauth2/scope-", | ||||
|                                                     ) || false; | ||||
|                                             } else { | ||||
|                                                 selected = Array.from( | ||||
|                                                     this.instance?.propertyMappings, | ||||
|                                                 ).some((su) => { | ||||
|                                                     return su == scope.pk; | ||||
|                                                 }); | ||||
|                                             } | ||||
|                                             return html`<option | ||||
|                                                 value=${ifDefined(scope.pk)} | ||||
|                                                 ?selected=${selected} | ||||
|                                             > | ||||
|                                                 ${scope.name} | ||||
|                                             </option>`; | ||||
|                                         }); | ||||
|                                     }), | ||||
|                                 html`<option>${t`Loading...`}</option>`, | ||||
|                             )} | ||||
|                         </select> | ||||
|                         <p class="pf-c-form__helper-text"> | ||||
|                             ${t`Select which scopes can be used by the client. The client still has to specify the scope to access the data.`} | ||||
|                         </p> | ||||
|                         <p class="pf-c-form__helper-text"> | ||||
|                             ${t`Hold control/command to select multiple items.`} | ||||
|                         </p> | ||||
|                     </ak-form-element-horizontal> | ||||
|                     <ak-form-element-horizontal | ||||
|                         label=${t`Subject mode`} | ||||
|                         ?required=${true} | ||||
|                         name="subMode" | ||||
|                     > | ||||
|                         <select class="pf-c-form-control"> | ||||
|                             <option | ||||
|                                 value="${SubModeEnum.HashedUserId}" | ||||
|                                 ?selected=${this.instance?.subMode === SubModeEnum.HashedUserId} | ||||
|                             > | ||||
|                                 ${t`Based on the Hashed User ID`} | ||||
|                             </option> | ||||
|                             <option | ||||
|                                 value="${SubModeEnum.UserUsername}" | ||||
|                                 ?selected=${this.instance?.subMode === SubModeEnum.UserUsername} | ||||
|                             > | ||||
|                                 ${t`Based on the username`} | ||||
|                             </option> | ||||
|                             <option | ||||
|                                 value="${SubModeEnum.UserEmail}" | ||||
|                                 ?selected=${this.instance?.subMode === SubModeEnum.UserEmail} | ||||
|                             > | ||||
|                                 ${t`Based on the User's Email. This is recommended over the UPN method.`} | ||||
|                             </option> | ||||
|                             <option | ||||
|                                 value="${SubModeEnum.UserUpn}" | ||||
|                                 ?selected=${this.instance?.subMode === SubModeEnum.UserUpn} | ||||
|                             > | ||||
|                                 ${t`Based on the User's UPN, only works if user has a 'upn' attribute set. Use this method only if you have different UPN and Mail domains.`} | ||||
|                             </option> | ||||
|                         </select> | ||||
|                         <p class="pf-c-form__helper-text"> | ||||
|                             ${t`Configure what data should be used as unique User Identifier. For most cases, the default should be fine.`} | ||||
|                         </p> | ||||
|                     </ak-form-element-horizontal> | ||||
|                     <ak-form-element-horizontal name="includeClaimsInIdToken"> | ||||
|                         <div class="pf-c-check"> | ||||
|                             <input | ||||
|                                 type="checkbox" | ||||
|                                 class="pf-c-check__input" | ||||
|                                 ?checked=${first(this.instance?.includeClaimsInIdToken, true)} | ||||
|                             /> | ||||
|                             <label class="pf-c-check__label"> | ||||
|                                 ${t`Include claims in id_token`} | ||||
|                             </label> | ||||
|                         </div> | ||||
|                         <p class="pf-c-form__helper-text"> | ||||
|                             ${t`Include User claims from scopes in the id_token, for applications that don't access the userinfo endpoint.`} | ||||
|                         </p> | ||||
|                     </ak-form-element-horizontal> | ||||
|                     <ak-form-element-horizontal | ||||
|                         label=${t`Issuer mode`} | ||||
|                         ?required=${true} | ||||
|                         name="issuerMode" | ||||
|                     > | ||||
|                         <select class="pf-c-form-control"> | ||||
|                             <option | ||||
|                                 value="${IssuerModeEnum.PerProvider}" | ||||
|                                 ?selected=${this.instance?.issuerMode === | ||||
|                                 IssuerModeEnum.PerProvider} | ||||
|                             > | ||||
|                                 ${t`Each provider has a different issuer, based on the application slug.`} | ||||
|                             </option> | ||||
|                             <option | ||||
|                                 value="${IssuerModeEnum.Global}" | ||||
|                                 ?selected=${this.instance?.issuerMode === IssuerModeEnum.Global} | ||||
|                             > | ||||
|                                 ${t`Same identifier is used for all providers`} | ||||
|                             </option> | ||||
|                         </select> | ||||
|                         <p class="pf-c-form__helper-text"> | ||||
|                             ${t`Configure how the issuer field of the ID Token should be filled.`} | ||||
|                         </p> | ||||
|                     </ak-form-element-horizontal> | ||||
|                 </div> | ||||
|             </ak-form-group> | ||||
|  | ||||
|             <ak-form-group> | ||||
|                 <span slot="header">${t`Machine-to-Machine authentication settings`}</span> | ||||
|                 <div slot="body" class="pf-c-form"> | ||||
|                     <ak-form-element-horizontal label=${t`Trusted OIDC Sources`} name="jwksSources"> | ||||
|                         <select class="pf-c-form-control" multiple> | ||||
|                             ${until( | ||||
|                                 new SourcesApi(DEFAULT_CONFIG) | ||||
|                                     .sourcesOauthList({ | ||||
|                                         ordering: "name", | ||||
|                                     }) | ||||
|                                     .then((sources) => { | ||||
|                                         return sources.results.map((source) => { | ||||
|                                             const selected = ( | ||||
|                                                 this.instance?.jwksSources || [] | ||||
|                                             ).some((su) => { | ||||
|                                                 return su == source.pk; | ||||
|                                             }); | ||||
|                                             return html`<option | ||||
|                                                 value=${source.pk} | ||||
|                                                 ?selected=${selected} | ||||
|                                             > | ||||
|                                                 ${source.name} (${source.slug}) | ||||
|                                             </option>`; | ||||
|                                         }); | ||||
|                                     }), | ||||
|                                 html`<option>${t`Loading...`}</option>`, | ||||
|                             )} | ||||
|                         </select> | ||||
|                         <p class="pf-c-form__helper-text"> | ||||
|                             ${t`Deprecated. Instead of using this field, configure the JWKS data/URL in Sources.`} | ||||
|                         </p> | ||||
|                         <p class="pf-c-form__helper-text"> | ||||
|                             ${t`JWTs signed by certificates configured here can be used to authenticate to the provider.`} | ||||
|                         </p> | ||||
|                         <p class="pf-c-form__helper-text"> | ||||
|                             ${t`Hold control/command to select multiple items.`} | ||||
|                         </p> | ||||
|                     </ak-form-element-horizontal> | ||||
|                 </div> | ||||
|             </ak-form-group> | ||||
|         </form>`; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										281
									
								
								web/src/admin/providers/oauth2/OAuth2ProviderViewPage.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										281
									
								
								web/src/admin/providers/oauth2/OAuth2ProviderViewPage.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,281 @@ | ||||
| import "@goauthentik/admin/providers/RelatedApplicationButton"; | ||||
| import "@goauthentik/admin/providers/oauth2/OAuth2ProviderForm"; | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { EVENT_REFRESH } from "@goauthentik/common/constants"; | ||||
| import { convertToTitle } from "@goauthentik/common/utils"; | ||||
| import { AKElement } from "@goauthentik/elements/Base"; | ||||
| import "@goauthentik/elements/CodeMirror"; | ||||
| import "@goauthentik/elements/Tabs"; | ||||
| import "@goauthentik/elements/buttons/ModalButton"; | ||||
| import "@goauthentik/elements/buttons/SpinnerButton"; | ||||
| import "@goauthentik/elements/events/ObjectChangelog"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { CSSResult, TemplateResult, html } from "lit"; | ||||
| import { customElement, property } from "lit/decorators.js"; | ||||
|  | ||||
| import AKGlobal from "@goauthentik/common/styles/authentik.css"; | ||||
| import PFBanner from "@patternfly/patternfly/components/Banner/banner.css"; | ||||
| import PFButton from "@patternfly/patternfly/components/Button/button.css"; | ||||
| import PFCard from "@patternfly/patternfly/components/Card/card.css"; | ||||
| import PFContent from "@patternfly/patternfly/components/Content/content.css"; | ||||
| import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css"; | ||||
| import PFForm from "@patternfly/patternfly/components/Form/form.css"; | ||||
| import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css"; | ||||
| import PFPage from "@patternfly/patternfly/components/Page/page.css"; | ||||
| import PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css"; | ||||
| import PFBase from "@patternfly/patternfly/patternfly-base.css"; | ||||
|  | ||||
| import { OAuth2Provider, OAuth2ProviderSetupURLs, ProvidersApi } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-provider-oauth2-view") | ||||
| export class OAuth2ProviderViewPage extends AKElement { | ||||
|     @property({ type: Number }) | ||||
|     set providerID(value: number) { | ||||
|         const api = new ProvidersApi(DEFAULT_CONFIG); | ||||
|         api.providersOauth2Retrieve({ | ||||
|             id: value, | ||||
|         }).then((prov) => { | ||||
|             this.provider = prov; | ||||
|         }); | ||||
|         api.providersOauth2SetupUrlsRetrieve({ | ||||
|             id: value, | ||||
|         }).then((prov) => { | ||||
|             this.providerUrls = prov; | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     @property({ attribute: false }) | ||||
|     provider?: OAuth2Provider; | ||||
|  | ||||
|     @property({ attribute: false }) | ||||
|     providerUrls?: OAuth2ProviderSetupURLs; | ||||
|  | ||||
|     static get styles(): CSSResult[] { | ||||
|         return [ | ||||
|             PFBase, | ||||
|             PFButton, | ||||
|             PFPage, | ||||
|             PFGrid, | ||||
|             PFContent, | ||||
|             PFCard, | ||||
|             PFDescriptionList, | ||||
|             PFForm, | ||||
|             PFFormControl, | ||||
|             PFBanner, | ||||
|             AKGlobal, | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     constructor() { | ||||
|         super(); | ||||
|         this.addEventListener(EVENT_REFRESH, () => { | ||||
|             if (!this.provider?.pk) return; | ||||
|             this.providerID = this.provider?.pk; | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     render(): TemplateResult { | ||||
|         if (!this.provider) { | ||||
|             return html``; | ||||
|         } | ||||
|         return html` ${ | ||||
|             this.provider?.assignedApplicationName | ||||
|                 ? html`` | ||||
|                 : html`<div slot="header" class="pf-c-banner pf-m-warning"> | ||||
|                       ${t`Warning: Provider is not used by an Application.`} | ||||
|                   </div>` | ||||
|         } | ||||
|             <div class="pf-c-page__main-section pf-m-no-padding-mobile pf-l-grid pf-m-gutter"> | ||||
|                 <div class="pf-c-card pf-l-grid__item pf-m-12-col"> | ||||
|                             <div class="pf-c-card"> | ||||
|                                 <div class="pf-c-card__body"> | ||||
|                                     <dl class="pf-c-description-list pf-m-2-col-on-lg"> | ||||
|                                         <div class="pf-c-description-list__group"> | ||||
|                                             <dt class="pf-c-description-list__term"> | ||||
|                                                 <span class="pf-c-description-list__text" | ||||
|                                                     >${t`Name`}</span | ||||
|                                                 > | ||||
|                                             </dt> | ||||
|                                             <dd class="pf-c-description-list__description"> | ||||
|                                                 <div class="pf-c-description-list__text"> | ||||
|                                                     ${this.provider.name} | ||||
|                                                 </div> | ||||
|                                             </dd> | ||||
|                                         </div> | ||||
|                                         <div class="pf-c-description-list__group"> | ||||
|                                             <dt class="pf-c-description-list__term"> | ||||
|                                                 <span class="pf-c-description-list__text" | ||||
|                                                     >${t`Assigned to application`}</span | ||||
|                                                 > | ||||
|                                             </dt> | ||||
|                                             <dd class="pf-c-description-list__description"> | ||||
|                                                 <div class="pf-c-description-list__text"> | ||||
|                                                     <ak-provider-related-application | ||||
|                                                         .provider=${this.provider} | ||||
|                                                     ></ak-provider-related-application> | ||||
|                                                 </div> | ||||
|                                             </dd> | ||||
|                                         </div> | ||||
|                                         <div class="pf-c-description-list__group"> | ||||
|                                             <dt class="pf-c-description-list__term"> | ||||
|                                                 <span class="pf-c-description-list__text" | ||||
|                                                     >${t`Client type`}</span | ||||
|                                                 > | ||||
|                                             </dt> | ||||
|                                             <dd class="pf-c-description-list__description"> | ||||
|                                                 <div class="pf-c-description-list__text"> | ||||
|                                                     ${convertToTitle( | ||||
|                                                         this.provider.clientType || "", | ||||
|                                                     )} | ||||
|                                                 </div> | ||||
|                                             </dd> | ||||
|                                         </div> | ||||
|                                         <div class="pf-c-description-list__group"> | ||||
|                                             <dt class="pf-c-description-list__term"> | ||||
|                                                 <span class="pf-c-description-list__text" | ||||
|                                                     >${t`Client ID`}</span | ||||
|                                                 > | ||||
|                                             </dt> | ||||
|                                             <dd class="pf-c-description-list__description"> | ||||
|                                                 <div class="pf-c-description-list__text"> | ||||
|                                                     ${this.provider.clientId} | ||||
|                                                 </div> | ||||
|                                             </dd> | ||||
|                                         </div> | ||||
|                                         <div class="pf-c-description-list__group"> | ||||
|                                             <dt class="pf-c-description-list__term"> | ||||
|                                                 <span class="pf-c-description-list__text" | ||||
|                                                     >${t`Redirect URIs`}</span | ||||
|                                                 > | ||||
|                                             </dt> | ||||
|                                             <dd class="pf-c-description-list__description"> | ||||
|                                                 <div class="pf-c-description-list__text"> | ||||
|                                                     ${this.provider.redirectUris} | ||||
|                                                 </div> | ||||
|                                             </dd> | ||||
|                                         </div> | ||||
|                                     </dl> | ||||
|                                 </div> | ||||
|                                 <div class="pf-c-card__footer"> | ||||
|                                     <ak-forms-modal> | ||||
|                                         <span slot="submit"> ${t`Update`} </span> | ||||
|                                         <span slot="header"> ${t`Update OAuth2 Provider`} </span> | ||||
|                                         <ak-provider-oauth2-form | ||||
|                                             slot="form" | ||||
|                                             .instancePk=${this.provider.pk || 0} | ||||
|                                         > | ||||
|                                         </ak-provider-oauth2-form> | ||||
|                                         <button slot="trigger" class="pf-c-button pf-m-primary"> | ||||
|                                             ${t`Edit`} | ||||
|                                         </button> | ||||
|                                     </ak-forms-modal> | ||||
|                                 </div> | ||||
|                             </div> | ||||
|                         </div> | ||||
|                         <div class="pf-c-card pf-l-grid__item pf-m-12-col"> | ||||
|                             <div class="pf-c-card"> | ||||
|                                 <div class="pf-c-card__body"> | ||||
|                                     <form class="pf-c-form"> | ||||
|                                         <div class="pf-c-form__group"> | ||||
|                                             <label class="pf-c-form__label"> | ||||
|                                                 <span class="pf-c-form__label-text" | ||||
|                                                     >${t`OpenID Configuration URL`}</span | ||||
|                                                 > | ||||
|                                             </label> | ||||
|                                             <input | ||||
|                                                 class="pf-c-form-control" | ||||
|                                                 readonly | ||||
|                                                 type="text" | ||||
|                                                 value="${this.providerUrls?.providerInfo || t`-`}" | ||||
|                                             /> | ||||
|                                         </div> | ||||
|                                         <div class="pf-c-form__group"> | ||||
|                                             <label class="pf-c-form__label"> | ||||
|                                                 <span class="pf-c-form__label-text" | ||||
|                                                     >${t`OpenID Configuration Issuer`}</span | ||||
|                                                 > | ||||
|                                             </label> | ||||
|                                             <input | ||||
|                                                 class="pf-c-form-control" | ||||
|                                                 readonly | ||||
|                                                 type="text" | ||||
|                                                 value="${this.providerUrls?.issuer || t`-`}" | ||||
|                                             /> | ||||
|                                         </div> | ||||
|                                         <hr /> | ||||
|                                         <div class="pf-c-form__group"> | ||||
|                                             <label class="pf-c-form__label"> | ||||
|                                                 <span class="pf-c-form__label-text" | ||||
|                                                     >${t`Authorize URL`}</span | ||||
|                                                 > | ||||
|                                             </label> | ||||
|                                             <input | ||||
|                                                 class="pf-c-form-control" | ||||
|                                                 readonly | ||||
|                                                 type="text" | ||||
|                                                 value="${this.providerUrls?.authorize || t`-`}" | ||||
|                                             /> | ||||
|                                         </div> | ||||
|                                         <div class="pf-c-form__group"> | ||||
|                                             <label class="pf-c-form__label"> | ||||
|                                                 <span class="pf-c-form__label-text" | ||||
|                                                     >${t`Token URL`}</span | ||||
|                                                 > | ||||
|                                             </label> | ||||
|                                             <input | ||||
|                                                 class="pf-c-form-control" | ||||
|                                                 readonly | ||||
|                                                 type="text" | ||||
|                                                 value="${this.providerUrls?.token || t`-`}" | ||||
|                                             /> | ||||
|                                         </div> | ||||
|                                         <div class="pf-c-form__group"> | ||||
|                                             <label class="pf-c-form__label"> | ||||
|                                                 <span class="pf-c-form__label-text" | ||||
|                                                     >${t`Userinfo URL`}</span | ||||
|                                                 > | ||||
|                                             </label> | ||||
|                                             <input | ||||
|                                                 class="pf-c-form-control" | ||||
|                                                 readonly | ||||
|                                                 type="text" | ||||
|                                                 value="${this.providerUrls?.userInfo || t`-`}" | ||||
|                                             /> | ||||
|                                         </div> | ||||
|                                         <div class="pf-c-form__group"> | ||||
|                                             <label class="pf-c-form__label"> | ||||
|                                                 <span class="pf-c-form__label-text" | ||||
|                                                     >${t`Logout URL`}</span | ||||
|                                                 > | ||||
|                                             </label> | ||||
|                                             <input | ||||
|                                                 class="pf-c-form-control" | ||||
|                                                 readonly | ||||
|                                                 type="text" | ||||
|                                                 value="${this.providerUrls?.logout || t`-`}" | ||||
|                                             /> | ||||
|                                         </div> | ||||
|                                         <div class="pf-c-form__group"> | ||||
|                                             <label class="pf-c-form__label"> | ||||
|                                                 <span class="pf-c-form__label-text" | ||||
|                                                     >${t`JWKS URL`}</span | ||||
|                                                 > | ||||
|                                             </label> | ||||
|                                             <input | ||||
|                                                 class="pf-c-form-control" | ||||
|                                                 readonly | ||||
|                                                 type="text" | ||||
|                                                 value="${this.providerUrls?.jwks || t`-`}" | ||||
|                                             /> | ||||
|                                         </div> | ||||
|                                     </form> | ||||
|                                 </div> | ||||
|                             </div> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                 </section> | ||||
|             </ak-tabs>`; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										449
									
								
								web/src/admin/providers/proxy/ProxyProviderForm.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										449
									
								
								web/src/admin/providers/proxy/ProxyProviderForm.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,449 @@ | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { first } from "@goauthentik/common/utils"; | ||||
| import "@goauthentik/elements/forms/FormGroup"; | ||||
| import "@goauthentik/elements/forms/HorizontalFormElement"; | ||||
| import { ModelForm } from "@goauthentik/elements/forms/ModelForm"; | ||||
| import "@goauthentik/elements/utils/TimeDeltaHelp"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { CSSResult, css } from "lit"; | ||||
| import { TemplateResult, html } from "lit"; | ||||
| import { customElement, property } from "lit/decorators.js"; | ||||
| import { ifDefined } from "lit/directives/if-defined.js"; | ||||
| import { until } from "lit/directives/until.js"; | ||||
|  | ||||
| import PFContent from "@patternfly/patternfly/components/Content/content.css"; | ||||
| import PFList from "@patternfly/patternfly/components/List/list.css"; | ||||
| import PFToggleGroup from "@patternfly/patternfly/components/ToggleGroup/toggle-group.css"; | ||||
| import PFSpacing from "@patternfly/patternfly/utilities/Spacing/spacing.css"; | ||||
|  | ||||
| import { | ||||
|     CryptoApi, | ||||
|     FlowsApi, | ||||
|     FlowsInstancesListDesignationEnum, | ||||
|     PropertymappingsApi, | ||||
|     ProvidersApi, | ||||
|     ProxyMode, | ||||
|     ProxyProvider, | ||||
| } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-provider-proxy-form") | ||||
| export class ProxyProviderFormPage extends ModelForm<ProxyProvider, number> { | ||||
|     static get styles(): CSSResult[] { | ||||
|         return super.styles.concat( | ||||
|             PFToggleGroup, | ||||
|             PFContent, | ||||
|             PFList, | ||||
|             PFSpacing, | ||||
|             css` | ||||
|                 .pf-c-toggle-group { | ||||
|                     justify-content: center; | ||||
|                 } | ||||
|             `, | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     loadInstance(pk: number): Promise<ProxyProvider> { | ||||
|         return new ProvidersApi(DEFAULT_CONFIG) | ||||
|             .providersProxyRetrieve({ | ||||
|                 id: pk, | ||||
|             }) | ||||
|             .then((provider) => { | ||||
|                 this.showHttpBasic = first(provider.basicAuthEnabled, true); | ||||
|                 this.mode = first(provider.mode, ProxyMode.Proxy); | ||||
|                 return provider; | ||||
|             }); | ||||
|     } | ||||
|  | ||||
|     @property({ type: Boolean }) | ||||
|     showHttpBasic = true; | ||||
|  | ||||
|     @property({ attribute: false }) | ||||
|     mode: ProxyMode = ProxyMode.Proxy; | ||||
|  | ||||
|     getSuccessMessage(): string { | ||||
|         if (this.instance) { | ||||
|             return t`Successfully updated provider.`; | ||||
|         } else { | ||||
|             return t`Successfully created provider.`; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     send = (data: ProxyProvider): Promise<ProxyProvider> => { | ||||
|         data.mode = this.mode; | ||||
|         if (this.instance) { | ||||
|             return new ProvidersApi(DEFAULT_CONFIG).providersProxyUpdate({ | ||||
|                 id: this.instance.pk || 0, | ||||
|                 proxyProviderRequest: data, | ||||
|             }); | ||||
|         } else { | ||||
|             return new ProvidersApi(DEFAULT_CONFIG).providersProxyCreate({ | ||||
|                 proxyProviderRequest: data, | ||||
|             }); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     renderHttpBasic(): TemplateResult { | ||||
|         if (!this.showHttpBasic) { | ||||
|             return html``; | ||||
|         } | ||||
|         return html`<ak-form-element-horizontal | ||||
|                 label=${t`HTTP-Basic Username Key`} | ||||
|                 name="basicAuthUserAttribute" | ||||
|             > | ||||
|                 <input | ||||
|                     type="text" | ||||
|                     value="${ifDefined(this.instance?.basicAuthUserAttribute)}" | ||||
|                     class="pf-c-form-control" | ||||
|                 /> | ||||
|                 <p class="pf-c-form__helper-text"> | ||||
|                     ${t`User/Group Attribute used for the user part of the HTTP-Basic Header. If not set, the user's Email address is used.`} | ||||
|                 </p> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-element-horizontal | ||||
|                 label=${t`HTTP-Basic Password Key`} | ||||
|                 name="basicAuthPasswordAttribute" | ||||
|             > | ||||
|                 <input | ||||
|                     type="text" | ||||
|                     value="${ifDefined(this.instance?.basicAuthPasswordAttribute)}" | ||||
|                     class="pf-c-form-control" | ||||
|                 /> | ||||
|                 <p class="pf-c-form__helper-text"> | ||||
|                     ${t`User/Group Attribute used for the password part of the HTTP-Basic Header.`} | ||||
|                 </p> | ||||
|             </ak-form-element-horizontal>`; | ||||
|     } | ||||
|  | ||||
|     renderModeSelector(): TemplateResult { | ||||
|         return html` <div class="pf-c-toggle-group__item"> | ||||
|                 <button | ||||
|                     class="pf-c-toggle-group__button ${this.mode === ProxyMode.Proxy | ||||
|                         ? "pf-m-selected" | ||||
|                         : ""}" | ||||
|                     type="button" | ||||
|                     @click=${() => { | ||||
|                         this.mode = ProxyMode.Proxy; | ||||
|                     }} | ||||
|                 > | ||||
|                     <span class="pf-c-toggle-group__text">${t`Proxy`}</span> | ||||
|                 </button> | ||||
|             </div> | ||||
|             <div class="pf-c-divider pf-m-vertical" role="separator"></div> | ||||
|             <div class="pf-c-toggle-group__item"> | ||||
|                 <button | ||||
|                     class="pf-c-toggle-group__button ${this.mode === ProxyMode.ForwardSingle | ||||
|                         ? "pf-m-selected" | ||||
|                         : ""}" | ||||
|                     type="button" | ||||
|                     @click=${() => { | ||||
|                         this.mode = ProxyMode.ForwardSingle; | ||||
|                     }} | ||||
|                 > | ||||
|                     <span class="pf-c-toggle-group__text" | ||||
|                         >${t`Forward auth (single application)`}</span | ||||
|                     > | ||||
|                 </button> | ||||
|             </div> | ||||
|             <div class="pf-c-divider pf-m-vertical" role="separator"></div> | ||||
|             <div class="pf-c-toggle-group__item"> | ||||
|                 <button | ||||
|                     class="pf-c-toggle-group__button ${this.mode === ProxyMode.ForwardDomain | ||||
|                         ? "pf-m-selected" | ||||
|                         : ""}" | ||||
|                     type="button" | ||||
|                     @click=${() => { | ||||
|                         this.mode = ProxyMode.ForwardDomain; | ||||
|                     }} | ||||
|                 > | ||||
|                     <span class="pf-c-toggle-group__text">${t`Forward auth (domain level)`}</span> | ||||
|                 </button> | ||||
|             </div>`; | ||||
|     } | ||||
|  | ||||
|     renderSettings(): TemplateResult { | ||||
|         switch (this.mode) { | ||||
|             case ProxyMode.Proxy: | ||||
|                 return html`<p class="pf-u-mb-xl"> | ||||
|                         ${t`This provider will behave like a transparent reverse-proxy, except requests must be authenticated. If your upstream application uses HTTPS, make sure to connect to the outpost using HTTPS as well.`} | ||||
|                     </p> | ||||
|                     <ak-form-element-horizontal | ||||
|                         label=${t`External host`} | ||||
|                         ?required=${true} | ||||
|                         name="externalHost" | ||||
|                     > | ||||
|                         <input | ||||
|                             type="text" | ||||
|                             value="${ifDefined(this.instance?.externalHost)}" | ||||
|                             class="pf-c-form-control" | ||||
|                             required | ||||
|                         /> | ||||
|                         <p class="pf-c-form__helper-text"> | ||||
|                             ${t`The external URL you'll access the application at. Include any non-standard port.`} | ||||
|                         </p> | ||||
|                     </ak-form-element-horizontal> | ||||
|                     <ak-form-element-horizontal | ||||
|                         label=${t`Internal host`} | ||||
|                         ?required=${true} | ||||
|                         name="internalHost" | ||||
|                     > | ||||
|                         <input | ||||
|                             type="text" | ||||
|                             value="${ifDefined(this.instance?.internalHost)}" | ||||
|                             class="pf-c-form-control" | ||||
|                             required | ||||
|                         /> | ||||
|                         <p class="pf-c-form__helper-text"> | ||||
|                             ${t`Upstream host that the requests are forwarded to.`} | ||||
|                         </p> | ||||
|                     </ak-form-element-horizontal> | ||||
|                     <ak-form-element-horizontal name="internalHostSslValidation"> | ||||
|                         <div class="pf-c-check"> | ||||
|                             <input | ||||
|                                 type="checkbox" | ||||
|                                 class="pf-c-check__input" | ||||
|                                 ?checked=${first(this.instance?.internalHostSslValidation, true)} | ||||
|                             /> | ||||
|                             <label class="pf-c-check__label"> | ||||
|                                 ${t`Internal host SSL Validation`} | ||||
|                             </label> | ||||
|                         </div> | ||||
|                         <p class="pf-c-form__helper-text"> | ||||
|                             ${t`Validate SSL Certificates of upstream servers.`} | ||||
|                         </p> | ||||
|                     </ak-form-element-horizontal>`; | ||||
|             case ProxyMode.ForwardSingle: | ||||
|                 return html`<p class="pf-u-mb-xl"> | ||||
|                         ${t`Use this provider with nginx's auth_request or traefik's forwardAuth. Each application/domain needs its own provider. Additionally, on each domain, /outpost.goauthentik.io must be routed to the outpost (when using a manged outpost, this is done for you).`} | ||||
|                     </p> | ||||
|                     <ak-form-element-horizontal | ||||
|                         label=${t`External host`} | ||||
|                         ?required=${true} | ||||
|                         name="externalHost" | ||||
|                     > | ||||
|                         <input | ||||
|                             type="text" | ||||
|                             value="${ifDefined(this.instance?.externalHost)}" | ||||
|                             class="pf-c-form-control" | ||||
|                             required | ||||
|                         /> | ||||
|                         <p class="pf-c-form__helper-text"> | ||||
|                             ${t`The external URL you'll access the application at. Include any non-standard port.`} | ||||
|                         </p> | ||||
|                     </ak-form-element-horizontal>`; | ||||
|             case ProxyMode.ForwardDomain: | ||||
|                 return html`<p class="pf-u-mb-xl"> | ||||
|                         ${t`Use this provider with nginx's auth_request or traefik's forwardAuth. Only a single provider is required per root domain. You can't do per-application authorization, but you don't have to create a provider for each application.`} | ||||
|                     </p> | ||||
|                     <div class="pf-u-mb-xl"> | ||||
|                         ${t`An example setup can look like this:`} | ||||
|                         <ul class="pf-c-list"> | ||||
|                             <li>${t`authentik running on auth.example.com`}</li> | ||||
|                             <li>${t`app1 running on app1.example.com`}</li> | ||||
|                         </ul> | ||||
|                         ${t`In this case, you'd set the Authentication URL to auth.example.com and Cookie domain to example.com.`} | ||||
|                     </div> | ||||
|                     <ak-form-element-horizontal | ||||
|                         label=${t`Authentication URL`} | ||||
|                         ?required=${true} | ||||
|                         name="externalHost" | ||||
|                     > | ||||
|                         <input | ||||
|                             type="text" | ||||
|                             value="${first(this.instance?.externalHost, window.location.origin)}" | ||||
|                             class="pf-c-form-control" | ||||
|                             required | ||||
|                         /> | ||||
|                         <p class="pf-c-form__helper-text"> | ||||
|                             ${t`The external URL you'll authenticate at. The authentik core server should be reachable under this URL.`} | ||||
|                         </p> | ||||
|                     </ak-form-element-horizontal> | ||||
|                     <ak-form-element-horizontal | ||||
|                         label=${t`Cookie domain`} | ||||
|                         name="cookieDomain" | ||||
|                         ?required=${true} | ||||
|                     > | ||||
|                         <input | ||||
|                             type="text" | ||||
|                             value="${ifDefined(this.instance?.cookieDomain)}" | ||||
|                             class="pf-c-form-control" | ||||
|                             required | ||||
|                         /> | ||||
|                         <p class="pf-c-form__helper-text"> | ||||
|                             ${t`Set this to the domain you wish the authentication to be valid for. Must be a parent domain of the URL above. If you're running applications as app1.domain.tld, app2.domain.tld, set this to 'domain.tld'.`} | ||||
|                         </p> | ||||
|                     </ak-form-element-horizontal>`; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     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="${ifDefined(this.instance?.name)}" | ||||
|                     class="pf-c-form-control" | ||||
|                     required | ||||
|                 /> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-element-horizontal | ||||
|                 label=${t`Authorization flow`} | ||||
|                 ?required=${true} | ||||
|                 name="authorizationFlow" | ||||
|             > | ||||
|                 <select class="pf-c-form-control"> | ||||
|                     ${until( | ||||
|                         new FlowsApi(DEFAULT_CONFIG) | ||||
|                             .flowsInstancesList({ | ||||
|                                 ordering: "slug", | ||||
|                                 designation: FlowsInstancesListDesignationEnum.Authorization, | ||||
|                             }) | ||||
|                             .then((flows) => { | ||||
|                                 return flows.results.map((flow) => { | ||||
|                                     return html`<option | ||||
|                                         value=${ifDefined(flow.pk)} | ||||
|                                         ?selected=${this.instance?.authorizationFlow === flow.pk} | ||||
|                                     > | ||||
|                                         ${flow.name} (${flow.slug}) | ||||
|                                     </option>`; | ||||
|                                 }); | ||||
|                             }), | ||||
|                         html`<option>${t`Loading...`}</option>`, | ||||
|                     )} | ||||
|                 </select> | ||||
|                 <p class="pf-c-form__helper-text"> | ||||
|                     ${t`Flow used when authorizing this provider.`} | ||||
|                 </p> | ||||
|             </ak-form-element-horizontal> | ||||
|  | ||||
|             <div class="pf-c-card pf-m-selectable pf-m-selected"> | ||||
|                 <div class="pf-c-card__body"> | ||||
|                     <div class="pf-c-toggle-group">${this.renderModeSelector()}</div> | ||||
|                 </div> | ||||
|                 <div class="pf-c-card__footer">${this.renderSettings()}</div> | ||||
|             </div> | ||||
|             <ak-form-element-horizontal label=${t`Token validity`} name="tokenValidity"> | ||||
|                 <input | ||||
|                     type="text" | ||||
|                     value="${first(this.instance?.tokenValidity, "hours=24")}" | ||||
|                     class="pf-c-form-control" | ||||
|                 /> | ||||
|                 <p class="pf-c-form__helper-text">${t`Configure how long tokens are valid for.`}</p> | ||||
|                 <ak-utils-time-delta-help></ak-utils-time-delta-help> | ||||
|             </ak-form-element-horizontal> | ||||
|  | ||||
|             <ak-form-group> | ||||
|                 <span slot="header">${t`Advanced protocol settings`}</span> | ||||
|                 <div slot="body" class="pf-c-form"> | ||||
|                     <ak-form-element-horizontal label=${t`Certificate`} name="certificate"> | ||||
|                         <select class="pf-c-form-control"> | ||||
|                             <option value="" ?selected=${this.instance?.certificate === undefined}> | ||||
|                                 --------- | ||||
|                             </option> | ||||
|                             ${until( | ||||
|                                 new CryptoApi(DEFAULT_CONFIG) | ||||
|                                     .cryptoCertificatekeypairsList({ | ||||
|                                         ordering: "name", | ||||
|                                         hasKey: true, | ||||
|                                     }) | ||||
|                                     .then((keys) => { | ||||
|                                         return keys.results.map((key) => { | ||||
|                                             return html`<option | ||||
|                                                 value=${ifDefined(key.pk)} | ||||
|                                                 ?selected=${this.instance?.certificate === key.pk} | ||||
|                                             > | ||||
|                                                 ${key.name} | ||||
|                                             </option>`; | ||||
|                                         }); | ||||
|                                     }), | ||||
|                                 html`<option | ||||
|                                     value=${ifDefined(this.instance?.certificate || undefined)} | ||||
|                                     ?selected=${this.instance?.certificate !== undefined} | ||||
|                                 > | ||||
|                                     ${t`Loading...`} | ||||
|                                 </option>`, | ||||
|                             )} | ||||
|                         </select> | ||||
|                     </ak-form-element-horizontal> | ||||
|                     <ak-form-element-horizontal label=${t`Scopes`} name="propertyMappings"> | ||||
|                         <select class="pf-c-form-control" multiple> | ||||
|                             ${until( | ||||
|                                 new PropertymappingsApi(DEFAULT_CONFIG) | ||||
|                                     .propertymappingsScopeList({ | ||||
|                                         ordering: "scope_name", | ||||
|                                     }) | ||||
|                                     .then((scopes) => { | ||||
|                                         return scopes.results | ||||
|                                             .filter((scope) => { | ||||
|                                                 return !scope.managed?.startsWith( | ||||
|                                                     "goauthentik.io/providers", | ||||
|                                                 ); | ||||
|                                             }) | ||||
|                                             .map((scope) => { | ||||
|                                                 const selected = ( | ||||
|                                                     this.instance?.propertyMappings || [] | ||||
|                                                 ).some((su) => { | ||||
|                                                     return su == scope.pk; | ||||
|                                                 }); | ||||
|                                                 return html`<option | ||||
|                                                     value=${ifDefined(scope.pk)} | ||||
|                                                     ?selected=${selected} | ||||
|                                                 > | ||||
|                                                     ${scope.name} | ||||
|                                                 </option>`; | ||||
|                                             }); | ||||
|                                     }), | ||||
|                                 html`<option>${t`Loading...`}</option>`, | ||||
|                             )} | ||||
|                         </select> | ||||
|                         <p class="pf-c-form__helper-text"> | ||||
|                             ${t`Additional scope mappings, which are passed to the proxy.`} | ||||
|                         </p> | ||||
|                         <p class="pf-c-form__helper-text"> | ||||
|                             ${t`Hold control/command to select multiple items.`} | ||||
|                         </p> | ||||
|                     </ak-form-element-horizontal> | ||||
|  | ||||
|                     <ak-form-element-horizontal | ||||
|                         label="${this.mode === ProxyMode.ForwardDomain | ||||
|                             ? t`Unauthenticated URLs` | ||||
|                             : t`Unauthenticated Paths`}" | ||||
|                         name="skipPathRegex" | ||||
|                     > | ||||
|                         <textarea class="pf-c-form-control"> | ||||
| ${this.instance?.skipPathRegex}</textarea | ||||
|                         > | ||||
|                         <p class="pf-c-form__helper-text"> | ||||
|                             ${t`Regular expressions for which authentication is not required. Each new line is interpreted as a new expression.`} | ||||
|                         </p> | ||||
|                         <p class="pf-c-form__helper-text"> | ||||
|                             ${t`When using proxy or forward auth (single application) mode, the requested URL Path is checked against the regular expressions. When using forward auth (domain mode), the full requested URL including scheme and host is matched against the regular expressions.`} | ||||
|                         </p> | ||||
|                     </ak-form-element-horizontal> | ||||
|  | ||||
|                     <ak-form-element-horizontal name="basicAuthEnabled"> | ||||
|                         <div class="pf-c-check"> | ||||
|                             <input | ||||
|                                 type="checkbox" | ||||
|                                 class="pf-c-check__input" | ||||
|                                 ?checked=${first(this.instance?.basicAuthEnabled, false)} | ||||
|                                 @change=${(ev: Event) => { | ||||
|                                     const el = ev.target as HTMLInputElement; | ||||
|                                     this.showHttpBasic = el.checked; | ||||
|                                 }} | ||||
|                             /> | ||||
|                             <label class="pf-c-check__label"> | ||||
|                                 ${t`Set HTTP-Basic Authentication`} | ||||
|                             </label> | ||||
|                         </div> | ||||
|                         <p class="pf-c-form__helper-text"> | ||||
|                             ${t`Set a custom HTTP-Basic Authentication header based on values from authentik.`} | ||||
|                         </p> | ||||
|                     </ak-form-element-horizontal> | ||||
|                     ${this.renderHttpBasic()} | ||||
|                 </div> | ||||
|             </ak-form-group> | ||||
|         </form>`; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										313
									
								
								web/src/admin/providers/proxy/ProxyProviderViewPage.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										313
									
								
								web/src/admin/providers/proxy/ProxyProviderViewPage.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,313 @@ | ||||
| import "@goauthentik/admin/providers/RelatedApplicationButton"; | ||||
| import "@goauthentik/admin/providers/proxy/ProxyProviderForm"; | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { EVENT_REFRESH } from "@goauthentik/common/constants"; | ||||
| import { convertToSlug } from "@goauthentik/common/utils"; | ||||
| import MDCaddyStandalone from "@goauthentik/docs/providers/proxy/_caddy_standalone.md"; | ||||
| import MDNginxIngress from "@goauthentik/docs/providers/proxy/_nginx_ingress.md"; | ||||
| import MDNginxPM from "@goauthentik/docs/providers/proxy/_nginx_proxy_manager.md"; | ||||
| import MDNginxStandalone from "@goauthentik/docs/providers/proxy/_nginx_standalone.md"; | ||||
| import MDTraefikCompose from "@goauthentik/docs/providers/proxy/_traefik_compose.md"; | ||||
| import MDTraefikIngress from "@goauthentik/docs/providers/proxy/_traefik_ingress.md"; | ||||
| import MDTraefikStandalone from "@goauthentik/docs/providers/proxy/_traefik_standalone.md"; | ||||
| import { AKElement } from "@goauthentik/elements/Base"; | ||||
| import "@goauthentik/elements/CodeMirror"; | ||||
| import { PFColor } from "@goauthentik/elements/Label"; | ||||
| import { MarkdownDocument } from "@goauthentik/elements/Markdown"; | ||||
| import "@goauthentik/elements/Markdown"; | ||||
| import "@goauthentik/elements/Tabs"; | ||||
| import "@goauthentik/elements/buttons/ModalButton"; | ||||
| import "@goauthentik/elements/buttons/SpinnerButton"; | ||||
| import "@goauthentik/elements/events/ObjectChangelog"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { CSSResult, TemplateResult, html } from "lit"; | ||||
| import { customElement, property } from "lit/decorators.js"; | ||||
|  | ||||
| import AKGlobal from "@goauthentik/common/styles/authentik.css"; | ||||
| import PFBanner from "@patternfly/patternfly/components/Banner/banner.css"; | ||||
| import PFButton from "@patternfly/patternfly/components/Button/button.css"; | ||||
| import PFCard from "@patternfly/patternfly/components/Card/card.css"; | ||||
| import PFContent from "@patternfly/patternfly/components/Content/content.css"; | ||||
| import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css"; | ||||
| import PFForm from "@patternfly/patternfly/components/Form/form.css"; | ||||
| import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css"; | ||||
| import PFPage from "@patternfly/patternfly/components/Page/page.css"; | ||||
| import PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css"; | ||||
| import PFBase from "@patternfly/patternfly/patternfly-base.css"; | ||||
|  | ||||
| import { ProvidersApi, ProxyMode, ProxyProvider } from "@goauthentik/api"; | ||||
|  | ||||
| export function ModeToLabel(action?: ProxyMode): string { | ||||
|     if (!action) return ""; | ||||
|     switch (action) { | ||||
|         case ProxyMode.Proxy: | ||||
|             return t`Proxy`; | ||||
|         case ProxyMode.ForwardSingle: | ||||
|             return t`Forward auth (single application)`; | ||||
|         case ProxyMode.ForwardDomain: | ||||
|             return t`Forward auth (domain-level)`; | ||||
|     } | ||||
| } | ||||
|  | ||||
| export function isForward(mode: ProxyMode): boolean { | ||||
|     switch (mode) { | ||||
|         case ProxyMode.Proxy: | ||||
|             return false; | ||||
|         case ProxyMode.ForwardSingle: | ||||
|         case ProxyMode.ForwardDomain: | ||||
|             return true; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @customElement("ak-provider-proxy-view") | ||||
| export class ProxyProviderViewPage extends AKElement { | ||||
|     @property() | ||||
|     set args(value: { [key: string]: number }) { | ||||
|         this.providerID = value.id; | ||||
|     } | ||||
|  | ||||
|     @property({ type: Number }) | ||||
|     set providerID(value: number) { | ||||
|         new ProvidersApi(DEFAULT_CONFIG) | ||||
|             .providersProxyRetrieve({ | ||||
|                 id: value, | ||||
|             }) | ||||
|             .then((prov) => (this.provider = prov)); | ||||
|     } | ||||
|  | ||||
|     @property({ attribute: false }) | ||||
|     provider?: ProxyProvider; | ||||
|  | ||||
|     static get styles(): CSSResult[] { | ||||
|         return [ | ||||
|             PFBase, | ||||
|             PFButton, | ||||
|             PFPage, | ||||
|             PFGrid, | ||||
|             PFContent, | ||||
|             PFForm, | ||||
|             PFFormControl, | ||||
|             PFCard, | ||||
|             PFDescriptionList, | ||||
|             PFBanner, | ||||
|             AKGlobal, | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     constructor() { | ||||
|         super(); | ||||
|         this.addEventListener(EVENT_REFRESH, () => { | ||||
|             if (!this.provider?.pk) return; | ||||
|             this.providerID = this.provider?.pk; | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     renderConfigTemplate(markdown: MarkdownDocument): MarkdownDocument { | ||||
|         const extHost = new URL(this.provider?.externalHost || "http://a"); | ||||
|         // See website/docs/providers/proxy/forward_auth.mdx | ||||
|         if (this.provider?.mode === ProxyMode.ForwardSingle) { | ||||
|             markdown.html = markdown.html | ||||
|                 .replaceAll("authentik.company", window.location.hostname) | ||||
|                 .replaceAll("outpost.company:9000", window.location.hostname) | ||||
|                 .replaceAll("https://app.company", extHost.toString()) | ||||
|                 .replaceAll("app.company", extHost.hostname); | ||||
|         } else if (this.provider?.mode == ProxyMode.ForwardDomain) { | ||||
|             markdown.html = markdown.html | ||||
|                 .replaceAll("authentik.company", window.location.hostname) | ||||
|                 .replaceAll("outpost.company:9000", extHost.toString()) | ||||
|                 .replaceAll("https://app.company", extHost.toString()) | ||||
|                 .replaceAll("app.company", extHost.hostname); | ||||
|         } | ||||
|         return markdown; | ||||
|     } | ||||
|  | ||||
|     renderConfig(): TemplateResult { | ||||
|         const serves = [ | ||||
|             { | ||||
|                 label: t`Nginx (Ingress)`, | ||||
|                 md: MDNginxIngress, | ||||
|             }, | ||||
|             { | ||||
|                 label: t`Nginx (Proxy Manager)`, | ||||
|                 md: MDNginxPM, | ||||
|             }, | ||||
|             { | ||||
|                 label: t`Nginx (standalone)`, | ||||
|                 md: MDNginxStandalone, | ||||
|             }, | ||||
|             { | ||||
|                 label: t`Traefik (Ingress)`, | ||||
|                 md: MDTraefikIngress, | ||||
|             }, | ||||
|             { | ||||
|                 label: t`Traefik (Compose)`, | ||||
|                 md: MDTraefikCompose, | ||||
|             }, | ||||
|             { | ||||
|                 label: t`Traefik (Standalone)`, | ||||
|                 md: MDTraefikStandalone, | ||||
|             }, | ||||
|             { | ||||
|                 label: t`Caddy (Standalone)`, | ||||
|                 md: MDCaddyStandalone, | ||||
|             }, | ||||
|         ]; | ||||
|         return html`<ak-tabs pageIdentifier="proxy-setup"> | ||||
|             ${serves.map((server) => { | ||||
|                 return html`<section | ||||
|                     slot="page-${convertToSlug(server.label)}" | ||||
|                     data-tab-title="${server.label}" | ||||
|                     class="pf-c-page__main-section pf-m-light pf-m-no-padding-mobile" | ||||
|                 > | ||||
|                     <ak-markdown .md=${this.renderConfigTemplate(server.md)}></ak-markdown> | ||||
|                 </section>`; | ||||
|             })}</ak-tabs | ||||
|         >`; | ||||
|     } | ||||
|  | ||||
|     render(): TemplateResult { | ||||
|         if (!this.provider) { | ||||
|             return html``; | ||||
|         } | ||||
|         return html`${this.provider?.assignedApplicationName | ||||
|                 ? html`` | ||||
|                 : html`<div slot="header" class="pf-c-banner pf-m-warning"> | ||||
|                       ${t`Warning: Provider is not used by an Application.`} | ||||
|                   </div>`} | ||||
|             ${this.provider?.outpostSet.length < 1 | ||||
|                 ? html`<div slot="header" class="pf-c-banner pf-m-warning"> | ||||
|                       ${t`Warning: Provider is not used by any Outpost.`} | ||||
|                   </div>` | ||||
|                 : html``} | ||||
|             <div class="pf-c-page__main-section pf-m-no-padding-mobile pf-l-grid pf-m-gutter"> | ||||
|                 <div class="pf-c-card pf-l-grid__item pf-m-12-col"> | ||||
|                     <div class="pf-c-card__body"> | ||||
|                         <dl class="pf-c-description-list pf-m-3-col-on-lg"> | ||||
|                             <div class="pf-c-description-list__group"> | ||||
|                                 <dt class="pf-c-description-list__term"> | ||||
|                                     <span class="pf-c-description-list__text">${t`Name`}</span> | ||||
|                                 </dt> | ||||
|                                 <dd class="pf-c-description-list__description"> | ||||
|                                     <div class="pf-c-description-list__text"> | ||||
|                                         ${this.provider.name} | ||||
|                                     </div> | ||||
|                                 </dd> | ||||
|                             </div> | ||||
|                             <div class="pf-c-description-list__group"> | ||||
|                                 <dt class="pf-c-description-list__term"> | ||||
|                                     <span class="pf-c-description-list__text" | ||||
|                                         >${t`Assigned to application`}</span | ||||
|                                     > | ||||
|                                 </dt> | ||||
|                                 <dd class="pf-c-description-list__description"> | ||||
|                                     <div class="pf-c-description-list__text"> | ||||
|                                         <ak-provider-related-application | ||||
|                                             .provider=${this.provider} | ||||
|                                         ></ak-provider-related-application> | ||||
|                                     </div> | ||||
|                                 </dd> | ||||
|                             </div> | ||||
|                             <div class="pf-c-description-list__group"> | ||||
|                                 <dt class="pf-c-description-list__term"> | ||||
|                                     <span class="pf-c-description-list__text" | ||||
|                                         >${t`Internal Host`}</span | ||||
|                                     > | ||||
|                                 </dt> | ||||
|                                 <dd class="pf-c-description-list__description"> | ||||
|                                     <div class="pf-c-description-list__text"> | ||||
|                                         ${this.provider.internalHost} | ||||
|                                     </div> | ||||
|                                 </dd> | ||||
|                             </div> | ||||
|                             <div class="pf-c-description-list__group"> | ||||
|                                 <dt class="pf-c-description-list__term"> | ||||
|                                     <span class="pf-c-description-list__text" | ||||
|                                         >${t`External Host`}</span | ||||
|                                     > | ||||
|                                 </dt> | ||||
|                                 <dd class="pf-c-description-list__description"> | ||||
|                                     <div class="pf-c-description-list__text"> | ||||
|                                         <a target="_blank" href="${this.provider.externalHost}" | ||||
|                                             >${this.provider.externalHost}</a | ||||
|                                         > | ||||
|                                     </div> | ||||
|                                 </dd> | ||||
|                             </div> | ||||
|                             <div class="pf-c-description-list__group"> | ||||
|                                 <dt class="pf-c-description-list__term"> | ||||
|                                     <span class="pf-c-description-list__text" | ||||
|                                         >${t`Basic-Auth`}</span | ||||
|                                     > | ||||
|                                 </dt> | ||||
|                                 <dd class="pf-c-description-list__description"> | ||||
|                                     <div class="pf-c-description-list__text"> | ||||
|                                         <ak-label | ||||
|                                             color=${this.provider.basicAuthEnabled | ||||
|                                                 ? PFColor.Green | ||||
|                                                 : PFColor.Grey} | ||||
|                                         > | ||||
|                                             ${this.provider.basicAuthEnabled ? t`Yes` : t`No`} | ||||
|                                         </ak-label> | ||||
|                                     </div> | ||||
|                                 </dd> | ||||
|                             </div> | ||||
|                             <div class="pf-c-description-list__group"> | ||||
|                                 <dt class="pf-c-description-list__term"> | ||||
|                                     <span class="pf-c-description-list__text">${t`Mode`}</span> | ||||
|                                 </dt> | ||||
|                                 <dd class="pf-c-description-list__description"> | ||||
|                                     <div class="pf-c-description-list__text"> | ||||
|                                         ${ModeToLabel(this.provider.mode || ProxyMode.Proxy)} | ||||
|                                     </div> | ||||
|                                 </dd> | ||||
|                             </div> | ||||
|                         </dl> | ||||
|                     </div> | ||||
|                     <div class="pf-c-card__footer"> | ||||
|                         <ak-forms-modal> | ||||
|                             <span slot="submit"> ${t`Update`} </span> | ||||
|                             <span slot="header"> ${t`Update Proxy Provider`} </span> | ||||
|                             <ak-provider-proxy-form | ||||
|                                 slot="form" | ||||
|                                 .instancePk=${this.provider.pk || 0} | ||||
|                             > | ||||
|                             </ak-provider-proxy-form> | ||||
|                             <button slot="trigger" class="pf-c-button pf-m-primary"> | ||||
|                                 ${t`Edit`} | ||||
|                             </button> | ||||
|                         </ak-forms-modal> | ||||
|                     </div> | ||||
|                 </div> | ||||
|                 <div class="pf-c-card pf-l-grid__item pf-m-12-col"> | ||||
|                     <div class="pf-c-card__title">${t`Protocol Settings`}</div> | ||||
|                     <div class="pf-c-card__body"> | ||||
|                         <form class="pf-c-form"> | ||||
|                             <div class="pf-c-form__group"> | ||||
|                                 <label class="pf-c-form__label"> | ||||
|                                     <span class="pf-c-form__label-text" | ||||
|                                         >${t`Allowed Redirect URIs`}</span | ||||
|                                     > | ||||
|                                 </label> | ||||
|                                 <input | ||||
|                                     class="pf-c-form-control" | ||||
|                                     readonly | ||||
|                                     type="text" | ||||
|                                     value=${this.provider.redirectUris} | ||||
|                                 /> | ||||
|                             </div> | ||||
|                         </form> | ||||
|                     </div> | ||||
|                 </div> | ||||
|                 <div class="pf-c-card pf-l-grid__item pf-m-12-col"> | ||||
|                     <div class="pf-c-card__title">${t`Setup`}</div> | ||||
|                     <div class="pf-c-card__body"> | ||||
|                         ${isForward(this.provider?.mode || ProxyMode.Proxy) | ||||
|                             ? html` ${this.renderConfig()} ` | ||||
|                             : html` <p>${t`No additional setup is required.`}</p> `} | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </div>`; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										434
									
								
								web/src/admin/providers/saml/SAMLProviderForm.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										434
									
								
								web/src/admin/providers/saml/SAMLProviderForm.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,434 @@ | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import "@goauthentik/elements/forms/FormGroup"; | ||||
| import "@goauthentik/elements/forms/HorizontalFormElement"; | ||||
| import { ModelForm } from "@goauthentik/elements/forms/ModelForm"; | ||||
| import "@goauthentik/elements/utils/TimeDeltaHelp"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { TemplateResult, html } from "lit"; | ||||
| import { customElement } from "lit/decorators.js"; | ||||
| import { ifDefined } from "lit/directives/if-defined.js"; | ||||
| import { until } from "lit/directives/until.js"; | ||||
|  | ||||
| import { | ||||
|     CryptoApi, | ||||
|     DigestAlgorithmEnum, | ||||
|     FlowsApi, | ||||
|     FlowsInstancesListDesignationEnum, | ||||
|     PropertymappingsApi, | ||||
|     ProvidersApi, | ||||
|     SAMLProvider, | ||||
|     SignatureAlgorithmEnum, | ||||
|     SpBindingEnum, | ||||
| } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-provider-saml-form") | ||||
| export class SAMLProviderFormPage extends ModelForm<SAMLProvider, number> { | ||||
|     loadInstance(pk: number): Promise<SAMLProvider> { | ||||
|         return new ProvidersApi(DEFAULT_CONFIG).providersSamlRetrieve({ | ||||
|             id: pk, | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     getSuccessMessage(): string { | ||||
|         if (this.instance) { | ||||
|             return t`Successfully updated provider.`; | ||||
|         } else { | ||||
|             return t`Successfully created provider.`; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     send = (data: SAMLProvider): Promise<SAMLProvider> => { | ||||
|         if (this.instance) { | ||||
|             return new ProvidersApi(DEFAULT_CONFIG).providersSamlUpdate({ | ||||
|                 id: this.instance.pk || 0, | ||||
|                 sAMLProviderRequest: data, | ||||
|             }); | ||||
|         } else { | ||||
|             return new ProvidersApi(DEFAULT_CONFIG).providersSamlCreate({ | ||||
|                 sAMLProviderRequest: 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="${ifDefined(this.instance?.name)}" | ||||
|                     class="pf-c-form-control" | ||||
|                     required | ||||
|                 /> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-element-horizontal | ||||
|                 label=${t`Authorization flow`} | ||||
|                 ?required=${true} | ||||
|                 name="authorizationFlow" | ||||
|             > | ||||
|                 <select class="pf-c-form-control"> | ||||
|                     ${until( | ||||
|                         new FlowsApi(DEFAULT_CONFIG) | ||||
|                             .flowsInstancesList({ | ||||
|                                 ordering: "slug", | ||||
|                                 designation: FlowsInstancesListDesignationEnum.Authorization, | ||||
|                             }) | ||||
|                             .then((flows) => { | ||||
|                                 return flows.results.map((flow) => { | ||||
|                                     return html`<option | ||||
|                                         value=${ifDefined(flow.pk)} | ||||
|                                         ?selected=${this.instance?.authorizationFlow === flow.pk} | ||||
|                                     > | ||||
|                                         ${flow.name} (${flow.slug}) | ||||
|                                     </option>`; | ||||
|                                 }); | ||||
|                             }), | ||||
|                         html`<option>${t`Loading...`}</option>`, | ||||
|                     )} | ||||
|                 </select> | ||||
|                 <p class="pf-c-form__helper-text"> | ||||
|                     ${t`Flow used when authorizing this provider.`} | ||||
|                 </p> | ||||
|             </ak-form-element-horizontal> | ||||
|  | ||||
|             <ak-form-group .expanded=${true}> | ||||
|                 <span slot="header"> ${t`Protocol settings`} </span> | ||||
|                 <div slot="body" class="pf-c-form"> | ||||
|                     <ak-form-element-horizontal label=${t`ACS URL`} ?required=${true} name="acsUrl"> | ||||
|                         <input | ||||
|                             type="text" | ||||
|                             value="${ifDefined(this.instance?.acsUrl)}" | ||||
|                             class="pf-c-form-control" | ||||
|                             required | ||||
|                         /> | ||||
|                     </ak-form-element-horizontal> | ||||
|                     <ak-form-element-horizontal label=${t`Issuer`} ?required=${true} name="issuer"> | ||||
|                         <input | ||||
|                             type="text" | ||||
|                             value="${this.instance?.issuer || "authentik"}" | ||||
|                             class="pf-c-form-control" | ||||
|                             required | ||||
|                         /> | ||||
|                         <p class="pf-c-form__helper-text">${t`Also known as EntityID.`}</p> | ||||
|                     </ak-form-element-horizontal> | ||||
|                     <ak-form-element-horizontal | ||||
|                         label=${t`Service Provider Binding`} | ||||
|                         ?required=${true} | ||||
|                         name="spBinding" | ||||
|                     > | ||||
|                         <select class="pf-c-form-control"> | ||||
|                             <option | ||||
|                                 value=${SpBindingEnum.Redirect} | ||||
|                                 ?selected=${this.instance?.spBinding === SpBindingEnum.Redirect} | ||||
|                             > | ||||
|                                 ${t`Redirect`} | ||||
|                             </option> | ||||
|                             <option | ||||
|                                 value=${SpBindingEnum.Post} | ||||
|                                 ?selected=${this.instance?.spBinding === SpBindingEnum.Post} | ||||
|                             > | ||||
|                                 ${t`Post`} | ||||
|                             </option> | ||||
|                         </select> | ||||
|                         <p class="pf-c-form__helper-text"> | ||||
|                             ${t`Determines how authentik sends the response back to the Service Provider.`} | ||||
|                         </p> | ||||
|                     </ak-form-element-horizontal> | ||||
|                     <ak-form-element-horizontal label=${t`Audience`} name="audience"> | ||||
|                         <input | ||||
|                             type="text" | ||||
|                             value="${ifDefined(this.instance?.audience)}" | ||||
|                             class="pf-c-form-control" | ||||
|                         /> | ||||
|                     </ak-form-element-horizontal> | ||||
|                 </div> | ||||
|             </ak-form-group> | ||||
|  | ||||
|             <ak-form-group> | ||||
|                 <span slot="header"> ${t`Advanced protocol settings`} </span> | ||||
|                 <div slot="body" class="pf-c-form"> | ||||
|                     <ak-form-element-horizontal label=${t`Signing Certificate`} name="signingKp"> | ||||
|                         <select class="pf-c-form-control"> | ||||
|                             <option value="" ?selected=${this.instance?.signingKp === undefined}> | ||||
|                                 --------- | ||||
|                             </option> | ||||
|                             ${until( | ||||
|                                 new CryptoApi(DEFAULT_CONFIG) | ||||
|                                     .cryptoCertificatekeypairsList({ | ||||
|                                         ordering: "name", | ||||
|                                         hasKey: true, | ||||
|                                     }) | ||||
|                                     .then((keys) => { | ||||
|                                         return keys.results.map((key) => { | ||||
|                                             return html`<option | ||||
|                                                 value=${ifDefined(key.pk)} | ||||
|                                                 ?selected=${this.instance?.signingKp === key.pk} | ||||
|                                             > | ||||
|                                                 ${key.name} | ||||
|                                             </option>`; | ||||
|                                         }); | ||||
|                                     }), | ||||
|                                 html`<option | ||||
|                                     value=${ifDefined(this.instance?.signingKp || undefined)} | ||||
|                                     ?selected=${this.instance?.signingKp !== undefined} | ||||
|                                 > | ||||
|                                     ${t`Loading...`} | ||||
|                                 </option>`, | ||||
|                             )} | ||||
|                         </select> | ||||
|                         <p class="pf-c-form__helper-text"> | ||||
|                             ${t`Certificate used to sign outgoing Responses going to the Service Provider.`} | ||||
|                         </p> | ||||
|                     </ak-form-element-horizontal> | ||||
|                     <ak-form-element-horizontal | ||||
|                         label=${t`Verification Certificate`} | ||||
|                         name="verificationKp" | ||||
|                     > | ||||
|                         <select class="pf-c-form-control"> | ||||
|                             <option | ||||
|                                 value="" | ||||
|                                 ?selected=${this.instance?.verificationKp === undefined} | ||||
|                             > | ||||
|                                 --------- | ||||
|                             </option> | ||||
|                             ${until( | ||||
|                                 new CryptoApi(DEFAULT_CONFIG) | ||||
|                                     .cryptoCertificatekeypairsList({ | ||||
|                                         ordering: "name", | ||||
|                                     }) | ||||
|                                     .then((keys) => { | ||||
|                                         return keys.results.map((key) => { | ||||
|                                             return html`<option | ||||
|                                                 value=${ifDefined(key.pk)} | ||||
|                                                 ?selected=${this.instance?.verificationKp === | ||||
|                                                 key.pk} | ||||
|                                             > | ||||
|                                                 ${key.name} | ||||
|                                             </option>`; | ||||
|                                         }); | ||||
|                                     }), | ||||
|                                 html`<option | ||||
|                                     value=${ifDefined(this.instance?.verificationKp || undefined)} | ||||
|                                     ?selected=${this.instance?.verificationKp !== undefined} | ||||
|                                 > | ||||
|                                     ${t`Loading...`} | ||||
|                                 </option>`, | ||||
|                             )} | ||||
|                         </select> | ||||
|                         <p class="pf-c-form__helper-text"> | ||||
|                             ${t`When selected, incoming assertion's Signatures will be validated against this certificate. To allow unsigned Requests, leave on default.`} | ||||
|                         </p> | ||||
|                     </ak-form-element-horizontal> | ||||
|  | ||||
|                     <ak-form-element-horizontal | ||||
|                         label=${t`Property mappings`} | ||||
|                         ?required=${true} | ||||
|                         name="propertyMappings" | ||||
|                     > | ||||
|                         <select class="pf-c-form-control" multiple> | ||||
|                             ${until( | ||||
|                                 new PropertymappingsApi(DEFAULT_CONFIG) | ||||
|                                     .propertymappingsSamlList({ | ||||
|                                         ordering: "saml_name", | ||||
|                                     }) | ||||
|                                     .then((mappings) => { | ||||
|                                         return mappings.results.map((mapping) => { | ||||
|                                             let selected = false; | ||||
|                                             if (!this.instance?.propertyMappings) { | ||||
|                                                 selected = | ||||
|                                                     mapping.managed?.startsWith( | ||||
|                                                         "goauthentik.io/providers/saml", | ||||
|                                                     ) || false; | ||||
|                                             } else { | ||||
|                                                 selected = Array.from( | ||||
|                                                     this.instance?.propertyMappings, | ||||
|                                                 ).some((su) => { | ||||
|                                                     return su == mapping.pk; | ||||
|                                                 }); | ||||
|                                             } | ||||
|                                             return html`<option | ||||
|                                                 value=${ifDefined(mapping.pk)} | ||||
|                                                 ?selected=${selected} | ||||
|                                             > | ||||
|                                                 ${mapping.name} | ||||
|                                             </option>`; | ||||
|                                         }); | ||||
|                                     }), | ||||
|                                 html`<option>${t`Loading...`}</option>`, | ||||
|                             )} | ||||
|                         </select> | ||||
|                         <p class="pf-c-form__helper-text"> | ||||
|                             ${t`Hold control/command to select multiple items.`} | ||||
|                         </p> | ||||
|                     </ak-form-element-horizontal> | ||||
|                     <ak-form-element-horizontal | ||||
|                         label=${t`NameID Property Mapping`} | ||||
|                         name="nameIdMapping" | ||||
|                     > | ||||
|                         <select class="pf-c-form-control"> | ||||
|                             <option | ||||
|                                 value="" | ||||
|                                 ?selected=${this.instance?.nameIdMapping === undefined} | ||||
|                             > | ||||
|                                 --------- | ||||
|                             </option> | ||||
|                             ${until( | ||||
|                                 new PropertymappingsApi(DEFAULT_CONFIG) | ||||
|                                     .propertymappingsSamlList({ | ||||
|                                         ordering: "saml_name", | ||||
|                                     }) | ||||
|                                     .then((mappings) => { | ||||
|                                         return mappings.results.map((mapping) => { | ||||
|                                             return html`<option | ||||
|                                                 value=${ifDefined(mapping.pk)} | ||||
|                                                 ?selected=${this.instance?.nameIdMapping === | ||||
|                                                 mapping.pk} | ||||
|                                             > | ||||
|                                                 ${mapping.name} | ||||
|                                             </option>`; | ||||
|                                         }); | ||||
|                                     }), | ||||
|                                 html`<option>${t`Loading...`}</option>`, | ||||
|                             )} | ||||
|                         </select> | ||||
|                         <p class="pf-c-form__helper-text"> | ||||
|                             ${t`Configure how the NameID value will be created. When left empty, the NameIDPolicy of the incoming request will be respected.`} | ||||
|                         </p> | ||||
|                     </ak-form-element-horizontal> | ||||
|  | ||||
|                     <ak-form-element-horizontal | ||||
|                         label=${t`Assertion valid not before`} | ||||
|                         ?required=${true} | ||||
|                         name="assertionValidNotBefore" | ||||
|                     > | ||||
|                         <input | ||||
|                             type="text" | ||||
|                             value="${this.instance?.assertionValidNotBefore || "minutes=-5"}" | ||||
|                             class="pf-c-form-control" | ||||
|                             required | ||||
|                         /> | ||||
|                         <p class="pf-c-form__helper-text"> | ||||
|                             ${t`Configure the maximum allowed time drift for an assertion.`} | ||||
|                         </p> | ||||
|                         <ak-utils-time-delta-help></ak-utils-time-delta-help> | ||||
|                     </ak-form-element-horizontal> | ||||
|                     <ak-form-element-horizontal | ||||
|                         label=${t`Assertion valid not on or after`} | ||||
|                         ?required=${true} | ||||
|                         name="assertionValidNotOnOrAfter" | ||||
|                     > | ||||
|                         <input | ||||
|                             type="text" | ||||
|                             value="${this.instance?.assertionValidNotOnOrAfter || "minutes=5"}" | ||||
|                             class="pf-c-form-control" | ||||
|                             required | ||||
|                         /> | ||||
|                         <p class="pf-c-form__helper-text"> | ||||
|                             ${t`Assertion not valid on or after current time + this value.`} | ||||
|                         </p> | ||||
|                         <ak-utils-time-delta-help></ak-utils-time-delta-help> | ||||
|                     </ak-form-element-horizontal> | ||||
|                     <ak-form-element-horizontal | ||||
|                         label=${t`Session valid not on or after`} | ||||
|                         ?required=${true} | ||||
|                         name="sessionValidNotOnOrAfter" | ||||
|                     > | ||||
|                         <input | ||||
|                             type="text" | ||||
|                             value="${this.instance?.sessionValidNotOnOrAfter || "minutes=86400"}" | ||||
|                             class="pf-c-form-control" | ||||
|                             required | ||||
|                         /> | ||||
|                         <p class="pf-c-form__helper-text"> | ||||
|                             ${t`Session not valid on or after current time + this value.`} | ||||
|                         </p> | ||||
|                         <ak-utils-time-delta-help></ak-utils-time-delta-help> | ||||
|                     </ak-form-element-horizontal> | ||||
|  | ||||
|                     <ak-form-element-horizontal | ||||
|                         label=${t`Digest algorithm`} | ||||
|                         ?required=${true} | ||||
|                         name="digestAlgorithm" | ||||
|                     > | ||||
|                         <select class="pf-c-form-control"> | ||||
|                             <option | ||||
|                                 value=${DigestAlgorithmEnum._200009Xmldsigsha1} | ||||
|                                 ?selected=${this.instance?.digestAlgorithm === | ||||
|                                 DigestAlgorithmEnum._200009Xmldsigsha1} | ||||
|                             > | ||||
|                                 ${t`SHA1`} | ||||
|                             </option> | ||||
|                             <option | ||||
|                                 value=${DigestAlgorithmEnum._200104Xmlencsha256} | ||||
|                                 ?selected=${this.instance?.digestAlgorithm === | ||||
|                                     DigestAlgorithmEnum._200104Xmlencsha256 || | ||||
|                                 this.instance?.digestAlgorithm === undefined} | ||||
|                             > | ||||
|                                 ${t`SHA256`} | ||||
|                             </option> | ||||
|                             <option | ||||
|                                 value=${DigestAlgorithmEnum._200104XmldsigMoresha384} | ||||
|                                 ?selected=${this.instance?.digestAlgorithm === | ||||
|                                 DigestAlgorithmEnum._200104XmldsigMoresha384} | ||||
|                             > | ||||
|                                 ${t`SHA384`} | ||||
|                             </option> | ||||
|                             <option | ||||
|                                 value=${DigestAlgorithmEnum._200104Xmlencsha512} | ||||
|                                 ?selected=${this.instance?.digestAlgorithm === | ||||
|                                 DigestAlgorithmEnum._200104Xmlencsha512} | ||||
|                             > | ||||
|                                 ${t`SHA512`} | ||||
|                             </option> | ||||
|                         </select> | ||||
|                     </ak-form-element-horizontal> | ||||
|                     <ak-form-element-horizontal | ||||
|                         label=${t`Signature algorithm`} | ||||
|                         ?required=${true} | ||||
|                         name="signatureAlgorithm" | ||||
|                     > | ||||
|                         <select class="pf-c-form-control"> | ||||
|                             <option | ||||
|                                 value=${SignatureAlgorithmEnum._200009XmldsigrsaSha1} | ||||
|                                 ?selected=${this.instance?.signatureAlgorithm === | ||||
|                                 SignatureAlgorithmEnum._200009XmldsigrsaSha1} | ||||
|                             > | ||||
|                                 ${t`RSA-SHA1`} | ||||
|                             </option> | ||||
|                             <option | ||||
|                                 value=${SignatureAlgorithmEnum._200104XmldsigMorersaSha256} | ||||
|                                 ?selected=${this.instance?.signatureAlgorithm === | ||||
|                                     SignatureAlgorithmEnum._200104XmldsigMorersaSha256 || | ||||
|                                 this.instance?.signatureAlgorithm === undefined} | ||||
|                             > | ||||
|                                 ${t`RSA-SHA256`} | ||||
|                             </option> | ||||
|                             <option | ||||
|                                 value=${SignatureAlgorithmEnum._200104XmldsigMorersaSha384} | ||||
|                                 ?selected=${this.instance?.signatureAlgorithm === | ||||
|                                 SignatureAlgorithmEnum._200104XmldsigMorersaSha384} | ||||
|                             > | ||||
|                                 ${t`RSA-SHA384`} | ||||
|                             </option> | ||||
|                             <option | ||||
|                                 value=${SignatureAlgorithmEnum._200104XmldsigMorersaSha512} | ||||
|                                 ?selected=${this.instance?.signatureAlgorithm === | ||||
|                                 SignatureAlgorithmEnum._200104XmldsigMorersaSha512} | ||||
|                             > | ||||
|                                 ${t`RSA-SHA512`} | ||||
|                             </option> | ||||
|                             <option | ||||
|                                 value=${SignatureAlgorithmEnum._200009XmldsigdsaSha1} | ||||
|                                 ?selected=${this.instance?.signatureAlgorithm === | ||||
|                                 SignatureAlgorithmEnum._200009XmldsigdsaSha1} | ||||
|                             > | ||||
|                                 ${t`DSA-SHA1`} | ||||
|                             </option> | ||||
|                         </select> | ||||
|                     </ak-form-element-horizontal> | ||||
|                 </div> | ||||
|             </ak-form-group> | ||||
|         </form>`; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										75
									
								
								web/src/admin/providers/saml/SAMLProviderImportForm.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								web/src/admin/providers/saml/SAMLProviderImportForm.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,75 @@ | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { SentryIgnoredError } from "@goauthentik/common/errors"; | ||||
| import { Form } from "@goauthentik/elements/forms/Form"; | ||||
| import "@goauthentik/elements/forms/HorizontalFormElement"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { TemplateResult, html } from "lit"; | ||||
| import { customElement } from "lit/decorators.js"; | ||||
| import { until } from "lit/directives/until.js"; | ||||
|  | ||||
| import { | ||||
|     FlowsApi, | ||||
|     FlowsInstancesListDesignationEnum, | ||||
|     ProvidersApi, | ||||
|     SAMLProvider, | ||||
| } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-provider-saml-import-form") | ||||
| export class SAMLProviderImportForm extends Form<SAMLProvider> { | ||||
|     getSuccessMessage(): string { | ||||
|         return t`Successfully imported provider.`; | ||||
|     } | ||||
|  | ||||
|     // eslint-disable-next-line | ||||
|     send = (data: SAMLProvider): Promise<void> => { | ||||
|         const file = this.getFormFiles()["metadata"]; | ||||
|         if (!file) { | ||||
|             throw new SentryIgnoredError("No form data"); | ||||
|         } | ||||
|         return new ProvidersApi(DEFAULT_CONFIG).providersSamlImportMetadataCreate({ | ||||
|             file: file, | ||||
|             name: data.name, | ||||
|             authorizationFlow: data.authorizationFlow, | ||||
|         }); | ||||
|     }; | ||||
|  | ||||
|     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" class="pf-c-form-control" required /> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-element-horizontal | ||||
|                 label=${t`Authorization flow`} | ||||
|                 ?required=${true} | ||||
|                 name="authorizationFlow" | ||||
|             > | ||||
|                 <select class="pf-c-form-control"> | ||||
|                     ${until( | ||||
|                         new FlowsApi(DEFAULT_CONFIG) | ||||
|                             .flowsInstancesList({ | ||||
|                                 ordering: "slug", | ||||
|                                 designation: FlowsInstancesListDesignationEnum.Authorization, | ||||
|                             }) | ||||
|                             .then((flows) => { | ||||
|                                 return flows.results.map((flow) => { | ||||
|                                     return html`<option value=${flow.slug}> | ||||
|                                         ${flow.name} (${flow.slug}) | ||||
|                                     </option>`; | ||||
|                                 }); | ||||
|                             }), | ||||
|                         html`<option>${t`Loading...`}</option>`, | ||||
|                     )} | ||||
|                 </select> | ||||
|                 <p class="pf-c-form__helper-text"> | ||||
|                     ${t`Flow used when authorizing this provider.`} | ||||
|                 </p> | ||||
|             </ak-form-element-horizontal> | ||||
|  | ||||
|             <ak-form-element-horizontal label=${t`Metadata`} name="metadata"> | ||||
|                 <input type="file" value="" class="pf-c-form-control" /> | ||||
|             </ak-form-element-horizontal> | ||||
|         </form>`; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										296
									
								
								web/src/admin/providers/saml/SAMLProviderViewPage.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										296
									
								
								web/src/admin/providers/saml/SAMLProviderViewPage.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,296 @@ | ||||
| import "@goauthentik/admin/providers/RelatedApplicationButton"; | ||||
| import "@goauthentik/admin/providers/saml/SAMLProviderForm"; | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { EVENT_REFRESH } from "@goauthentik/common/constants"; | ||||
| import { AKElement } from "@goauthentik/elements/Base"; | ||||
| import "@goauthentik/elements/CodeMirror"; | ||||
| import "@goauthentik/elements/Tabs"; | ||||
| import "@goauthentik/elements/buttons/ActionButton"; | ||||
| import "@goauthentik/elements/buttons/ModalButton"; | ||||
| import "@goauthentik/elements/buttons/SpinnerButton"; | ||||
| import "@goauthentik/elements/events/ObjectChangelog"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { CSSResult, TemplateResult, html } from "lit"; | ||||
| import { customElement, property } from "lit/decorators.js"; | ||||
| import { ifDefined } from "lit/directives/if-defined.js"; | ||||
| import { until } from "lit/directives/until.js"; | ||||
|  | ||||
| import AKGlobal from "@goauthentik/common/styles/authentik.css"; | ||||
| import PFBanner from "@patternfly/patternfly/components/Banner/banner.css"; | ||||
| import PFButton from "@patternfly/patternfly/components/Button/button.css"; | ||||
| import PFCard from "@patternfly/patternfly/components/Card/card.css"; | ||||
| import PFContent from "@patternfly/patternfly/components/Content/content.css"; | ||||
| import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css"; | ||||
| import PFForm from "@patternfly/patternfly/components/Form/form.css"; | ||||
| import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css"; | ||||
| import PFPage from "@patternfly/patternfly/components/Page/page.css"; | ||||
| import PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css"; | ||||
| import PFBase from "@patternfly/patternfly/patternfly-base.css"; | ||||
|  | ||||
| import { CryptoApi, ProvidersApi, SAMLProvider } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-provider-saml-view") | ||||
| export class SAMLProviderViewPage extends AKElement { | ||||
|     @property() | ||||
|     set args(value: { [key: string]: number }) { | ||||
|         this.providerID = value.id; | ||||
|     } | ||||
|  | ||||
|     @property({ type: Number }) | ||||
|     set providerID(value: number) { | ||||
|         new ProvidersApi(DEFAULT_CONFIG) | ||||
|             .providersSamlRetrieve({ | ||||
|                 id: value, | ||||
|             }) | ||||
|             .then((prov) => (this.provider = prov)); | ||||
|     } | ||||
|  | ||||
|     @property({ attribute: false }) | ||||
|     provider?: SAMLProvider; | ||||
|  | ||||
|     static get styles(): CSSResult[] { | ||||
|         return [ | ||||
|             PFBase, | ||||
|             PFButton, | ||||
|             PFPage, | ||||
|             PFGrid, | ||||
|             PFContent, | ||||
|             PFCard, | ||||
|             PFDescriptionList, | ||||
|             PFForm, | ||||
|             PFFormControl, | ||||
|             PFBanner, | ||||
|             AKGlobal, | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     constructor() { | ||||
|         super(); | ||||
|         this.addEventListener(EVENT_REFRESH, () => { | ||||
|             if (!this.provider?.pk) return; | ||||
|             this.providerID = this.provider?.pk; | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     async renderRelatedObjects(): Promise<TemplateResult> { | ||||
|         if (!this.provider?.signingKp) { | ||||
|             return Promise.resolve(html``); | ||||
|         } | ||||
|         const kp = await new CryptoApi(DEFAULT_CONFIG).cryptoCertificatekeypairsRetrieve({ | ||||
|             kpUuid: this.provider.signingKp, | ||||
|         }); | ||||
|         return html` <div class="pf-c-card pf-l-grid__item pf-m-12-col"> | ||||
|             <div class="pf-c-card__title">${t`Related objects`}</div> | ||||
|             <div class="pf-c-card__body"> | ||||
|                 <dl class="pf-c-description-list pf-m-2-col"> | ||||
|                     <div class="pf-c-description-list__group"> | ||||
|                         <dt class="pf-c-description-list__term"> | ||||
|                             <span class="pf-c-description-list__text" | ||||
|                                 >${t`Download signing certificate`}</span | ||||
|                             > | ||||
|                         </dt> | ||||
|                         <dd class="pf-c-description-list__description"> | ||||
|                             <div class="pf-c-description-list__text"> | ||||
|                                 <a | ||||
|                                     class="pf-c-button pf-m-primary" | ||||
|                                     href=${kp.certificateDownloadUrl} | ||||
|                                     >${t`Download`}</a | ||||
|                                 > | ||||
|                             </div> | ||||
|                         </dd> | ||||
|                     </div> | ||||
|                 </dl> | ||||
|             </div> | ||||
|         </div>`; | ||||
|     } | ||||
|  | ||||
|     render(): TemplateResult { | ||||
|         if (!this.provider) { | ||||
|             return html``; | ||||
|         } | ||||
|         return html`${ | ||||
|             this.provider?.assignedApplicationName | ||||
|                 ? html`` | ||||
|                 : html`<div slot="header" class="pf-c-banner pf-m-warning"> | ||||
|                       ${t`Warning: Provider is not used by an Application.`} | ||||
|                   </div>` | ||||
|         } | ||||
|             <div class="pf-c-page__main-section pf-m-no-padding-mobile pf-l-grid pf-m-gutter"> | ||||
|                 <div class="pf-c-card pf-l-grid__item pf-m-12-col"> | ||||
|                     <div class="pf-c-card__body"> | ||||
|                         <dl class="pf-c-description-list pf-m-3-col-on-lg"> | ||||
|                             <div class="pf-c-description-list__group"> | ||||
|                                 <dt class="pf-c-description-list__term"> | ||||
|                                     <span class="pf-c-description-list__text">${t`Name`}</span> | ||||
|                                 </dt> | ||||
|                                 <dd class="pf-c-description-list__description"> | ||||
|                                     <div class="pf-c-description-list__text"> | ||||
|                                         ${this.provider.name} | ||||
|                                     </div> | ||||
|                                 </dd> | ||||
|                             </div> | ||||
|                             <div class="pf-c-description-list__group"> | ||||
|                                 <dt class="pf-c-description-list__term"> | ||||
|                                     <span class="pf-c-description-list__text" | ||||
|                                         >${t`Assigned to application`}</span | ||||
|                                     > | ||||
|                                 </dt> | ||||
|                                 <dd class="pf-c-description-list__description"> | ||||
|                                     <div class="pf-c-description-list__text"> | ||||
|                                         <ak-provider-related-application | ||||
|                                             .provider=${this.provider} | ||||
|                                         ></ak-provider-related-application> | ||||
|                                     </div> | ||||
|                                 </dd> | ||||
|                             </div> | ||||
|                             <div class="pf-c-description-list__group"> | ||||
|                                 <dt class="pf-c-description-list__term"> | ||||
|                                     <span class="pf-c-description-list__text">${t`ACS URL`}</span> | ||||
|                                 </dt> | ||||
|                                 <dd class="pf-c-description-list__description"> | ||||
|                                     <div class="pf-c-description-list__text"> | ||||
|                                         ${this.provider.acsUrl} | ||||
|                                     </div> | ||||
|                                 </dd> | ||||
|                             </div> | ||||
|                             <div class="pf-c-description-list__group"> | ||||
|                                 <dt class="pf-c-description-list__term"> | ||||
|                                     <span class="pf-c-description-list__text">${t`Audience`}</span> | ||||
|                                 </dt> | ||||
|                                 <dd class="pf-c-description-list__description"> | ||||
|                                     <div class="pf-c-description-list__text"> | ||||
|                                         ${this.provider.audience || "-"} | ||||
|                                     </div> | ||||
|                                 </dd> | ||||
|                             </div> | ||||
|                             <div class="pf-c-description-list__group"> | ||||
|                                 <dt class="pf-c-description-list__term"> | ||||
|                                     <span class="pf-c-description-list__text">${t`Issuer`}</span> | ||||
|                                 </dt> | ||||
|                                 <dd class="pf-c-description-list__description"> | ||||
|                                     <div class="pf-c-description-list__text"> | ||||
|                                         ${this.provider.issuer} | ||||
|                                     </div> | ||||
|                                 </dd> | ||||
|                             </div> | ||||
|                         </dl> | ||||
|                     </div> | ||||
|                     <div class="pf-c-card__footer"> | ||||
|                         <ak-forms-modal> | ||||
|                             <span slot="submit"> ${t`Update`} </span> | ||||
|                             <span slot="header"> ${t`Update SAML Provider`} </span> | ||||
|                             <ak-provider-saml-form slot="form" .instancePk=${this.provider.pk || 0}> | ||||
|                             </ak-provider-saml-form> | ||||
|                             <button slot="trigger" class="pf-c-button pf-m-primary"> | ||||
|                                 ${t`Edit`} | ||||
|                             </button> | ||||
|                         </ak-forms-modal> | ||||
|                     </div> | ||||
|                 </div> | ||||
|                 ${until(this.renderRelatedObjects())} | ||||
|                 ${ | ||||
|                     this.provider.assignedApplicationName | ||||
|                         ? html` <div class="pf-c-card pf-l-grid__item pf-m-12-col"> | ||||
|                                   <div class="pf-c-card__title">${t`SAML Configuration`}</div> | ||||
|                                   <div class="pf-c-card__body"> | ||||
|                                       <form class="pf-c-form"> | ||||
|                                           <div class="pf-c-form__group"> | ||||
|                                               <label class="pf-c-form__label"> | ||||
|                                                   <span class="pf-c-form__label-text" | ||||
|                                                       >${t`EntityID/Issuer`}</span | ||||
|                                                   > | ||||
|                                               </label> | ||||
|                                               <input | ||||
|                                                   class="pf-c-form-control" | ||||
|                                                   readonly | ||||
|                                                   type="text" | ||||
|                                                   value="${ifDefined(this.provider?.issuer)}" | ||||
|                                               /> | ||||
|                                           </div> | ||||
|                                           <div class="pf-c-form__group"> | ||||
|                                               <label class="pf-c-form__label"> | ||||
|                                                   <span class="pf-c-form__label-text" | ||||
|                                                       >${t`SSO URL (Post)`}</span | ||||
|                                                   > | ||||
|                                               </label> | ||||
|                                               <input | ||||
|                                                   class="pf-c-form-control" | ||||
|                                                   readonly | ||||
|                                                   type="text" | ||||
|                                                   value="${ifDefined(this.provider.urlSsoPost)}" | ||||
|                                               /> | ||||
|                                           </div> | ||||
|                                           <div class="pf-c-form__group"> | ||||
|                                               <label class="pf-c-form__label"> | ||||
|                                                   <span class="pf-c-form__label-text" | ||||
|                                                       >${t`SSO URL (Redirect)`}</span | ||||
|                                                   > | ||||
|                                               </label> | ||||
|                                               <input | ||||
|                                                   class="pf-c-form-control" | ||||
|                                                   readonly | ||||
|                                                   type="text" | ||||
|                                                   value="${ifDefined(this.provider.urlSsoRedirect)}" | ||||
|                                               /> | ||||
|                                           </div> | ||||
|                                           <div class="pf-c-form__group"> | ||||
|                                               <label class="pf-c-form__label"> | ||||
|                                                   <span class="pf-c-form__label-text" | ||||
|                                                       >${t`SSO URL (IdP-initiated Login)`}</span | ||||
|                                                   > | ||||
|                                               </label> | ||||
|                                               <input | ||||
|                                                   class="pf-c-form-control" | ||||
|                                                   readonly | ||||
|                                                   type="text" | ||||
|                                                   value="${ifDefined(this.provider.urlSsoInit)}" | ||||
|                                               /> | ||||
|                                           </div> | ||||
|                                       </form> | ||||
|                                   </div> | ||||
|                               </div> | ||||
|                               <div class="pf-c-card pf-l-grid__item pf-m-12-col"> | ||||
|                                   <div class="pf-c-card__title">${t`SAML Metadata`}</div> | ||||
|                                   <div class="pf-c-card__body"> | ||||
|                                       ${until( | ||||
|                                           new ProvidersApi(DEFAULT_CONFIG) | ||||
|                                               .providersSamlMetadataRetrieve({ | ||||
|                                                   id: this.provider.pk || 0, | ||||
|                                               }) | ||||
|                                               .then((m) => { | ||||
|                                                   return html`<ak-codemirror | ||||
|                                                       mode="xml" | ||||
|                                                       ?readOnly=${true} | ||||
|                                                       value="${ifDefined(m.metadata)}" | ||||
|                                                   ></ak-codemirror>`; | ||||
|                                               }), | ||||
|                                       )} | ||||
|                                   </div> | ||||
|                                   <div class="pf-c-card__footer"> | ||||
|                                       <a | ||||
|                                           class="pf-c-button pf-m-primary" | ||||
|                                           target="_blank" | ||||
|                                           href=${this.provider.urlDownloadMetadata} | ||||
|                                       > | ||||
|                                           ${t`Download`} | ||||
|                                       </a> | ||||
|                                       <ak-action-button | ||||
|                                           class="pf-m-secondary" | ||||
|                                           .apiRequest=${() => { | ||||
|                                               return navigator.clipboard.writeText( | ||||
|                                                   this.provider?.urlDownloadMetadata || "", | ||||
|                                               ); | ||||
|                                           }} | ||||
|                                       > | ||||
|                                           ${t`Copy download URL`} | ||||
|                                       </ak-action-button> | ||||
|                                   </div> | ||||
|                               </div>` | ||||
|                         : html`` | ||||
|                 } | ||||
|             </div> | ||||
|         </div>`; | ||||
|     } | ||||
| } | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user
	 Jens L
					Jens L