enterprise: initial enterprise (#5721)

* initial

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

* add user type

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

* add external users

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

* add ui, add more logic, add public JWT validation key

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

* revert to not use install_id as session jwt signing key

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

* fix more

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

* switch to PKI

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

* add more licensing stuff

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

* add install ID to form

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

* fix bugs

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

* start adding tests

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

* fixes

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

* use x5c correctly

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

* license checks

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

* use production CA

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

* more

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

* more UI stuff

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

* rename to summary

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

* update locale, improve ui

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

* add direct button

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

* update link

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

* format and such

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

* remove old attributes from ldap

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

* remove is_enterprise_licensed

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

* fix

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

* fix admin interface styling issue

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

* Update authentik/core/models.py

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Jens L. <jens@beryju.org>

* fix default case

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Signed-off-by: Jens L. <jens@beryju.org>
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
This commit is contained in:
Jens L
2023-07-17 17:57:08 +02:00
committed by GitHub
parent cf799fca03
commit 41af486006
56 changed files with 2534 additions and 128 deletions

View File

@ -11,6 +11,7 @@ import { me } from "@goauthentik/common/users";
import { WebsocketClient } from "@goauthentik/common/ws";
import { Interface } from "@goauthentik/elements/Base";
import "@goauthentik/elements/ak-locale-context";
import "@goauthentik/elements/enterprise/EnterpriseStatusBanner";
import "@goauthentik/elements/messages/MessageContainer";
import "@goauthentik/elements/messages/MessageContainer";
import "@goauthentik/elements/notifications/APIDrawer";
@ -30,7 +31,14 @@ 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, CoreApi, SessionUser, UiThemeEnum, Version } from "@goauthentik/api";
import {
AdminApi,
CapabilitiesEnum,
CoreApi,
SessionUser,
UiThemeEnum,
Version,
} from "@goauthentik/api";
@customElement("ak-interface-admin")
export class AdminInterface extends Interface {
@ -67,7 +75,17 @@ export class AdminInterface extends Interface {
.display-none {
display: none;
}
:host {
display: flex;
flex-direction: column;
height: 100%;
}
ak-locale-context {
display: flex;
flex-grow: 1;
}
.pf-c-page {
flex-grow: 1;
background-color: var(--pf-c-page--BackgroundColor) !important;
}
/* Global page background colour */
@ -113,7 +131,8 @@ export class AdminInterface extends Interface {
render(): TemplateResult {
return html` <ak-locale-context
><div class="pf-c-page">
><ak-enterprise-status interface="admin"></ak-enterprise-status>
<div class="pf-c-page">
<ak-sidebar
class="pf-c-page__sidebar ${this.sidebarOpen
? "pf-m-expanded"
@ -308,6 +327,16 @@ export class AdminInterface extends Interface {
<span slot="label">${msg("Outpost Integrations")}</span>
</ak-sidebar-item>
</ak-sidebar-item>
${this.config?.capabilities.includes(CapabilitiesEnum.IsEnterprise)
? html`
<ak-sidebar-item>
<span slot="label">${msg("Enterprise")}</span>
<ak-sidebar-item path="/enterprise/licenses">
<span slot="label">${msg("Licenses")}</span>
</ak-sidebar-item>
</ak-sidebar-item>
`
: html``}
`;
}
}

View File

@ -136,4 +136,8 @@ export const ROUTES: Route[] = [
await import("@goauthentik/admin/DebugPage");
return html`<ak-admin-debug-page></ak-admin-debug-page>`;
}),
new Route(new RegExp("^/enterprise/licenses$"), async () => {
await import("@goauthentik/admin/enterprise/EnterpriseLicenseListPage");
return html`<ak-enterprise-license-list></ak-enterprise-license-list>`;
}),
];

View File

@ -26,6 +26,7 @@ export class SystemStatusCard extends AdminStatusCard<System> {
// 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)
// TODO: Improve this in OOB flow
await this.setOutpostHost();
status = await new AdminApi(DEFAULT_CONFIG).adminSystemRetrieve();
}

View File

@ -0,0 +1,64 @@
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 { msg } from "@lit/localize";
import { TemplateResult, html } from "lit";
import { customElement, state } from "lit/decorators.js";
import { EnterpriseApi, License } from "@goauthentik/api";
@customElement("ak-enterprise-license-form")
export class EnterpriseLicenseForm extends ModelForm<License, string> {
@state()
installID?: string;
loadInstance(pk: string): Promise<License> {
return new EnterpriseApi(DEFAULT_CONFIG).enterpriseLicenseRetrieve({
licenseUuid: pk,
});
}
getSuccessMessage(): string {
if (this.instance) {
return msg("Successfully updated license.");
} else {
return msg("Successfully created license.");
}
}
async load(): Promise<void> {
this.installID = (
await new EnterpriseApi(DEFAULT_CONFIG).enterpriseLicenseGetInstallIdRetrieve()
).installId;
}
async send(data: License): Promise<License> {
if (this.instance) {
return new EnterpriseApi(DEFAULT_CONFIG).enterpriseLicensePartialUpdate({
licenseUuid: this.instance.licenseUuid || "",
patchedLicenseRequest: data,
});
} else {
return new EnterpriseApi(DEFAULT_CONFIG).enterpriseLicenseCreate({
licenseRequest: data,
});
}
}
renderForm(): TemplateResult {
return html`<form class="pf-c-form pf-m-horizontal">
<ak-form-element-horizontal label=${msg("Install ID")}>
<input class="pf-c-form-control" readonly type="text" value="${this.installID}" />
</ak-form-element-horizontal>
<ak-form-element-horizontal
name="key"
?writeOnly=${this.instance !== undefined}
label=${msg("License key")}
>
<textarea class="pf-c-form-control"></textarea>
</ak-form-element-horizontal>
</form>`;
}
}

View File

@ -0,0 +1,222 @@
import "@goauthentik/admin/enterprise/EnterpriseLicenseForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { uiConfig } from "@goauthentik/common/ui/config";
import { PFColor } from "@goauthentik/elements/Label";
import "@goauthentik/elements/Spinner";
import "@goauthentik/elements/buttons/SpinnerButton";
import "@goauthentik/elements/cards/AggregateCard";
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 { msg } from "@lit/localize";
import { CSSResult, TemplateResult, css, html } from "lit";
import { customElement, property, state } from "lit/decorators.js";
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 PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css";
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
import PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css";
import { EnterpriseApi, License, LicenseForecast, LicenseSummary } from "@goauthentik/api";
@customElement("ak-enterprise-license-list")
export class EnterpriseLicenseListPage extends TablePage<License> {
checkbox = true;
searchEnabled(): boolean {
return true;
}
pageTitle(): string {
return msg("Licenses");
}
pageDescription(): string {
return msg("Manage enterprise licenses");
}
pageIcon(): string {
return "pf-icon pf-icon-key";
}
@property()
order = "name";
@state()
forecast?: LicenseForecast;
@state()
summary?: LicenseSummary;
@state()
installID?: string;
static get styles(): CSSResult[] {
return super.styles.concat(
PFDescriptionList,
PFGrid,
PFBanner,
PFFormControl,
PFButton,
PFCard,
css`
.pf-m-no-padding-bottom {
padding-bottom: 0;
}
`,
);
}
async apiEndpoint(page: number): Promise<PaginatedResponse<License>> {
this.forecast = await new EnterpriseApi(DEFAULT_CONFIG).enterpriseLicenseForecastRetrieve();
this.summary = await new EnterpriseApi(DEFAULT_CONFIG).enterpriseLicenseSummaryRetrieve();
this.installID = (
await new EnterpriseApi(DEFAULT_CONFIG).enterpriseLicenseGetInstallIdRetrieve()
).installId;
return new EnterpriseApi(DEFAULT_CONFIG).enterpriseLicenseList({
ordering: this.order,
page: page,
pageSize: (await uiConfig()).pagination.perPage,
search: this.search || "",
});
}
columns(): TableColumn[] {
return [
new TableColumn(msg("Name"), "name"),
new TableColumn(msg("Users")),
new TableColumn(msg("Expiry date")),
new TableColumn(msg("Actions")),
];
}
renderToolbarSelected(): TemplateResult {
const disabled = this.selectedElements.length < 1;
return html`<ak-forms-delete-bulk
objectLabel=${msg("License(s)")}
.objects=${this.selectedElements}
.metadata=${(item: License) => {
return [
{ key: msg("Name"), value: item.name },
{ key: msg("Expiry"), value: item.expiry?.toLocaleString() },
];
}}
.usedBy=${(item: License) => {
return new EnterpriseApi(DEFAULT_CONFIG).enterpriseLicenseUsedByList({
licenseUuid: item.licenseUuid,
});
}}
.delete=${(item: License) => {
return new EnterpriseApi(DEFAULT_CONFIG).enterpriseLicenseDestroy({
licenseUuid: item.licenseUuid,
});
}}
>
<button ?disabled=${disabled} slot="trigger" class="pf-c-button pf-m-danger">
${msg("Delete")}
</button>
</ak-forms-delete-bulk>`;
}
renderSectionBefore(): TemplateResult {
return html`
<div class="pf-c-banner pf-m-info">
${msg("Enterprise is in preview.")}
<a href="mailto:hello@goauthentik.io">${msg("Send us feedback!")}</a>
</div>
<section class="pf-c-page__main-section pf-m-no-padding-bottom">
<div
class="pf-l-grid pf-m-gutter pf-m-all-6-col-on-sm pf-m-all-4-col-on-md pf-m-all-3-col-on-lg pf-m-all-3-col-on-xl"
>
<div class="pf-l-grid__item pf-c-card">
<div class="pf-c-card__title">${msg("How to get a license")}</div>
<div class="pf-c-card__body">
${this.installID
? html` <a
target="_blank"
href=${`https://customers.goauthentik.io/from_authentik/purchase/?install_id=${this.installID}`}
class="pf-c-button pf-m-primary pf-m-block"
>${msg("Go to the customer portal")}</a
>`
: html`<ak-spinner></ak-spinner>`}
</div>
</div>
<div class="pf-l-grid__item pf-c-card">
<ak-aggregate-card
icon="pf-icon pf-icon-user"
header=${msg("Forecasted default users")}
subtext=${msg("Estimated user count one year from now")}
>
${this.forecast?.users}
</ak-aggregate-card>
</div>
<div class="pf-l-grid__item pf-c-card">
<ak-aggregate-card
icon="pf-icon pf-icon-user"
header=${msg("Forecasted external users")}
subtext=${msg("Estimated external user count one year from now")}
>
${this.forecast?.externalUsers}
</ak-aggregate-card>
</div>
<div class="pf-l-grid__item pf-c-card">
<ak-aggregate-card
icon="pf-icon pf-icon-user"
header=${msg("Expiry")}
subtext=${msg("Cumulative license expiry")}
>
${this.summary?.hasLicense
? this.summary.latestValid.toLocaleString()
: "-"}
</ak-aggregate-card>
</div>
</div>
</section>
`;
}
row(item: License): TemplateResult[] {
let color = PFColor.Green;
if (item.expiry) {
const now = new Date();
const inAMonth = new Date();
inAMonth.setDate(inAMonth.getDate() + 30);
if (item.expiry <= inAMonth) {
color = PFColor.Orange;
}
if (item.expiry <= now) {
color = PFColor.Red;
}
}
return [
html`<div>${item.name}</div>`,
html`<div>
<small>0 / ${item.users}</small>
<small>0 / ${item.externalUsers}</small>
</div>`,
html`<ak-label color=${color}> ${item.expiry?.toLocaleString()} </ak-label>`,
html`<ak-forms-modal>
<span slot="submit"> ${msg("Update")} </span>
<span slot="header"> ${msg("Update License")} </span>
<ak-enterprise-license-form slot="form" .instancePk=${item.licenseUuid}>
</ak-enterprise-license-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"> ${msg("Create")} </span>
<span slot="header"> ${msg("Create License")} </span>
<ak-enterprise-license-form slot="form"> </ak-enterprise-license-form>
<button slot="trigger" class="pf-c-button pf-m-primary">${msg("Create")}</button>
</ak-forms-modal>
`;
}
}

View File

@ -1,9 +1,11 @@
import "@goauthentik/admin/users/GroupSelectModal";
import { UserTypeEnum } from "@goauthentik/api/dist/models/UserTypeEnum";
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 "@goauthentik/elements/forms/Radio";
import YAML from "yaml";
import { msg } from "@lit/localize";
@ -75,6 +77,31 @@ export class UserForm extends ModelForm<User, number> {
/>
<p class="pf-c-form__helper-text">${msg("User's display name.")}</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("User type")} ?required=${true} name="type">
<ak-radio
.options=${[
// TODO: Add better copy
{
label: "Default",
value: UserTypeEnum.Default,
default: true,
description: html`${msg("Default user")}`,
},
{
label: "External",
value: UserTypeEnum.External,
description: html`${msg("External user")}`,
},
{
label: "Service account",
value: UserTypeEnum.ServiceAccount,
description: html`${msg("Service account")}`,
},
]}
.value=${this.instance?.type}
>
</ak-radio>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("Email")} name="email">
<input
type="email"

View File

@ -19,6 +19,9 @@ export class AggregateCard extends AKElement {
@property()
headerLink?: string;
@property()
subtext?: string;
@property({ type: Boolean })
isCenter = true;
@ -79,6 +82,7 @@ export class AggregateCard extends AKElement {
</div>
<div class="pf-c-card__body ${this.isCenter ? "center-value" : ""}">
${this.renderInner()}
${this.subtext ? html`<p class="subtext">${this.subtext}</p>` : html``}
</div>
</div>`;
}

View File

@ -0,0 +1,52 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { AKElement } from "@goauthentik/elements/Base";
import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, html } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import PFBanner from "@patternfly/patternfly/components/Banner/banner.css";
import { EnterpriseApi, LicenseSummary } from "@goauthentik/api";
@customElement("ak-enterprise-status")
export class EnterpriseStatusBanner extends AKElement {
@state()
summary?: LicenseSummary;
@property()
interface: "admin" | "user" | "" = "";
static get styles(): CSSResult[] {
return [PFBanner];
}
firstUpdated(): void {
new EnterpriseApi(DEFAULT_CONFIG).enterpriseLicenseSummaryRetrieve().then((b) => {
this.summary = b;
});
}
renderBanner(): TemplateResult {
return html`<div class="pf-c-banner ${this.summary?.readOnly ? "pf-m-red" : "pf-m-orange"}">
${msg("Warning: The current user count has exceeded the configured licenses.")}
<a href="/if/admin/#/enterprise/licenses"> ${msg("Click here for more info.")} </a>
</div>`;
}
render(): TemplateResult {
switch (this.interface.toLowerCase()) {
case "admin":
if (this.summary?.showAdminWarning || this.summary?.readOnly) {
return this.renderBanner();
}
break;
case "user":
if (this.summary?.showUserWarning || this.summary?.readOnly) {
return this.renderBanner();
}
break;
}
return html``;
}
}

View File

@ -28,6 +28,16 @@ export abstract class TablePage<T> extends Table<T> {
return html``;
}
// Optionally render section above the table
renderSectionBefore(): TemplateResult {
return html``;
}
// Optionally render section below the table
renderSectionAfter(): TemplateResult {
return html``;
}
renderEmpty(inner?: TemplateResult): TemplateResult {
return super.renderEmpty(html`
${inner
@ -75,6 +85,7 @@ export abstract class TablePage<T> extends Table<T> {
description=${ifDefined(this.pageDescription())}
>
</ak-page-header>
${this.renderSectionBefore()}
<section class="pf-c-page__main-section pf-m-no-padding-mobile">
<div class="pf-c-sidebar pf-m-gutter">
<div class="pf-c-sidebar__main">
@ -85,6 +96,7 @@ export abstract class TablePage<T> extends Table<T> {
${this.renderSidebarAfter()}
</div>
</div>
</section>`;
</section>
${this.renderSectionAfter()}`;
}
}

View File

@ -11,6 +11,8 @@ import { first } from "@goauthentik/common/utils";
import { WebsocketClient } from "@goauthentik/common/ws";
import { Interface } from "@goauthentik/elements/Base";
import "@goauthentik/elements/ak-locale-context";
import "@goauthentik/elements/buttons/ActionButton";
import "@goauthentik/elements/enterprise/EnterpriseStatusBanner";
import "@goauthentik/elements/messages/MessageContainer";
import "@goauthentik/elements/notifications/APIDrawer";
import "@goauthentik/elements/notifications/NotificationDrawer";
@ -35,7 +37,7 @@ import PFPage from "@patternfly/patternfly/components/Page/page.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import PFDisplay from "@patternfly/patternfly/utilities/Display/display.css";
import { EventsApi, SessionUser } from "@goauthentik/api";
import { CoreApi, EventsApi, SessionUser } from "@goauthentik/api";
@customElement("ak-interface-user")
export class UserInterface extends Interface {
@ -148,6 +150,7 @@ export class UserInterface extends Interface {
userDisplay = this.me.user.username;
}
return html` <ak-locale-context>
<ak-enterprise-status interface="user"></ak-enterprise-status>
<div class="pf-c-page">
<div class="background-wrapper" style="${this.uiConfig.theme.background}"></div>
<header class="pf-c-page__header">
@ -243,18 +246,23 @@ export class UserInterface extends Interface {
: html``}
</div>
${this.me.original
? html`<div class="pf-c-page__header-tools">
<div class="pf-c-page__header-tools-group">
<a
class="pf-c-button pf-m-warning pf-m-small"
href=${`/-/impersonation/end/?back=${encodeURIComponent(
`${window.location.pathname}#${window.location.hash}`,
)}`}
>
${msg("Stop impersonation")}
</a>
</div>
</div>`
? html`&nbsp;
<div class="pf-c-page__header-tools">
<div class="pf-c-page__header-tools-group">
<ak-action-button
class="pf-m-warning pf-m-small"
.apiRequest=${() => {
return new CoreApi(DEFAULT_CONFIG)
.coreUsersImpersonateEndRetrieve()
.then(() => {
window.location.reload();
});
}}
>
${msg("Stop impersonation")}
</ak-action-button>
</div>
</div>`
: html``}
<div class="pf-c-page__header-tools-group">
<div