enterprise/providers/google: initial account sync to google workspace (#9384)
* providers/google: initial account sync to google workspace Signed-off-by: Jens Langhammer <jens@goauthentik.io> * start separating scim sync client Signed-off-by: Jens Langhammer <jens@goauthentik.io> * generalize more...ish Signed-off-by: Jens Langhammer <jens@goauthentik.io> * set dispatch_uid Signed-off-by: Jens Langhammer <jens@goauthentik.io> * start generalizing task Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fully separate tasks Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix more Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix signals...? Signed-off-by: Jens Langhammer <jens@goauthentik.io> * start google dedupe Signed-off-by: Jens Langhammer <jens@goauthentik.io> * drawing the rest of the owl Signed-off-by: Jens Langhammer <jens@goauthentik.io> * more Signed-off-by: Jens Langhammer <jens@goauthentik.io> * juse use a whole lot less magic Signed-off-by: Jens Langhammer <jens@goauthentik.io> * member sync, better implement conflict/retry-able exceptions Signed-off-by: Jens Langhammer <jens@goauthentik.io> * max wizards taller Signed-off-by: Jens Langhammer <jens@goauthentik.io> * gen api, basic UI Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix some bugs Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix a bunch more bugs Signed-off-by: Jens Langhammer <jens@goauthentik.io> * generalize sync status API Signed-off-by: Jens Langhammer <jens@goauthentik.io> * rework sync chart Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add slugify to evaluator Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add test property mappings Signed-off-by: Jens Langhammer <jens@goauthentik.io> * rename to google workspace Signed-off-by: Jens Langhammer <jens@goauthentik.io> * handle existing objects Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix credential render Signed-off-by: Jens Langhammer <jens@goauthentik.io> * verify email has correct domain before syncing user Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix missing docstring Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix lock not being used Signed-off-by: Jens Langhammer <jens@goauthentik.io> * abstract more common stuff away Signed-off-by: Jens Langhammer <jens@goauthentik.io> * backport time limit fix https://github.com/goauthentik/authentik/pull/9546 Signed-off-by: Jens Langhammer <jens@goauthentik.io> * start discovery Signed-off-by: Jens Langhammer <jens@goauthentik.io> * implement discover for google Signed-off-by: Jens Langhammer <jens@goauthentik.io> * prevent same issue as with https://github.com/goauthentik/authentik/pull/9557 Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix sync status Signed-off-by: Jens Langhammer <jens@goauthentik.io> * make group name unique in API Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix reference to old wrapper Signed-off-by: Jens Langhammer <jens@goauthentik.io> * start adding tests man this api client is awful Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add SkipObject Signed-off-by: Jens Langhammer <jens@goauthentik.io> * dont use weak ref Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add group tests Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add user and group delete options Signed-off-by: Jens Langhammer <jens@goauthentik.io> * set user agent Signed-off-by: Jens Langhammer <jens@goauthentik.io> * if the api's testing tools are awful, let's just make our own Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add more tests and already fix some more bugs Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add discover Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add preview banner Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add group import test Signed-off-by: Jens Langhammer <jens@goauthentik.io> * only import users/groups in the correct parent group Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix conflicting args Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix missing schedule Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix web ui Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add default_group_email_domain Signed-off-by: Jens Langhammer <jens@goauthentik.io> --------- Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
@ -1,4 +1,4 @@
|
||||
import { SyncStatus } from "@goauthentik/admin/admin-overview/charts/SyncStatusChart";
|
||||
import { SummarizedSyncStatus } from "@goauthentik/admin/admin-overview/charts/SyncStatusChart";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { AKChart } from "@goauthentik/elements/charts/Chart";
|
||||
import "@goauthentik/elements/forms/ConfirmationForm";
|
||||
@ -10,7 +10,7 @@ import { customElement } from "lit/decorators.js";
|
||||
import { OutpostsApi } from "@goauthentik/api";
|
||||
|
||||
@customElement("ak-admin-status-chart-outpost")
|
||||
export class OutpostStatusChart extends AKChart<SyncStatus[]> {
|
||||
export class OutpostStatusChart extends AKChart<SummarizedSyncStatus[]> {
|
||||
getChartType(): string {
|
||||
return "doughnut";
|
||||
}
|
||||
@ -26,16 +26,16 @@ export class OutpostStatusChart extends AKChart<SyncStatus[]> {
|
||||
};
|
||||
}
|
||||
|
||||
async apiRequest(): Promise<SyncStatus[]> {
|
||||
async apiRequest(): Promise<SummarizedSyncStatus[]> {
|
||||
const api = new OutpostsApi(DEFAULT_CONFIG);
|
||||
const outposts = await api.outpostsInstancesList({});
|
||||
const outpostStats: SyncStatus[] = [];
|
||||
const outpostStats: SummarizedSyncStatus[] = [];
|
||||
await Promise.all(
|
||||
outposts.results.map(async (element) => {
|
||||
const health = await api.outpostsInstancesHealthList({
|
||||
uuid: element.pk || "",
|
||||
});
|
||||
const singleStats: SyncStatus = {
|
||||
const singleStats: SummarizedSyncStatus = {
|
||||
unsynced: 0,
|
||||
healthy: 0,
|
||||
failed: 0,
|
||||
@ -59,7 +59,7 @@ export class OutpostStatusChart extends AKChart<SyncStatus[]> {
|
||||
return outpostStats;
|
||||
}
|
||||
|
||||
getChartData(data: SyncStatus[]): ChartData {
|
||||
getChartData(data: SummarizedSyncStatus[]): ChartData {
|
||||
return {
|
||||
labels: [msg("Healthy outposts"), msg("Outdated outposts"), msg("Unhealthy outposts")],
|
||||
datasets: data.map((d) => {
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { PaginatedResponse } from "@goauthentik/authentik/elements/table/Table";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { AKChart } from "@goauthentik/elements/charts/Chart";
|
||||
import "@goauthentik/elements/forms/ConfirmationForm";
|
||||
@ -6,9 +7,9 @@ import { ChartData, ChartOptions } from "chart.js";
|
||||
import { msg } from "@lit/localize";
|
||||
import { customElement } from "lit/decorators.js";
|
||||
|
||||
import { ProvidersApi, SourcesApi, SystemTaskStatusEnum } from "@goauthentik/api";
|
||||
import { ProvidersApi, SourcesApi, SyncStatus, SystemTaskStatusEnum } from "@goauthentik/api";
|
||||
|
||||
export interface SyncStatus {
|
||||
export interface SummarizedSyncStatus {
|
||||
healthy: number;
|
||||
failed: number;
|
||||
unsynced: number;
|
||||
@ -17,7 +18,7 @@ export interface SyncStatus {
|
||||
}
|
||||
|
||||
@customElement("ak-admin-status-chart-sync")
|
||||
export class LDAPSyncStatusChart extends AKChart<SyncStatus[]> {
|
||||
export class LDAPSyncStatusChart extends AKChart<SummarizedSyncStatus[]> {
|
||||
getChartType(): string {
|
||||
return "doughnut";
|
||||
}
|
||||
@ -33,99 +34,91 @@ export class LDAPSyncStatusChart extends AKChart<SyncStatus[]> {
|
||||
};
|
||||
}
|
||||
|
||||
async ldapStatus(): Promise<SyncStatus> {
|
||||
const api = new SourcesApi(DEFAULT_CONFIG);
|
||||
const sources = await api.sourcesLdapList({});
|
||||
async fetchStatus<T>(
|
||||
listObjects: () => Promise<PaginatedResponse<T>>,
|
||||
fetchSyncStatus: (element: T) => Promise<SyncStatus>,
|
||||
label: string,
|
||||
): Promise<SummarizedSyncStatus> {
|
||||
const objects = await listObjects();
|
||||
const metrics: { [key: string]: number } = {
|
||||
healthy: 0,
|
||||
failed: 0,
|
||||
unsynced: 0,
|
||||
};
|
||||
await Promise.all(
|
||||
sources.results.map(async (element) => {
|
||||
objects.results.map(async (element) => {
|
||||
// Each source should have 3 successful tasks, so the worst task overwrites
|
||||
let objectKey = "healthy";
|
||||
try {
|
||||
const health = await api.sourcesLdapSyncStatusRetrieve({
|
||||
slug: element.slug,
|
||||
});
|
||||
|
||||
health.tasks.forEach((task) => {
|
||||
const status = await fetchSyncStatus(element);
|
||||
status.tasks.forEach((task) => {
|
||||
if (task.status !== SystemTaskStatusEnum.Successful) {
|
||||
metrics.failed += 1;
|
||||
objectKey = "failed";
|
||||
}
|
||||
const now = new Date().getTime();
|
||||
const maxDelta = 3600000; // 1 hour
|
||||
if (!health || now - task.finishTimestamp.getTime() > maxDelta) {
|
||||
metrics.unsynced += 1;
|
||||
} else {
|
||||
metrics.healthy += 1;
|
||||
if (!status || now - task.finishTimestamp.getTime() > maxDelta) {
|
||||
objectKey = "unsynced";
|
||||
}
|
||||
});
|
||||
if (health.tasks.length < 1) {
|
||||
metrics.unsynced += 1;
|
||||
}
|
||||
} catch {
|
||||
metrics.unsynced += 1;
|
||||
objectKey = "unsynced";
|
||||
}
|
||||
metrics[objectKey] += 1;
|
||||
}),
|
||||
);
|
||||
return {
|
||||
healthy: metrics.healthy,
|
||||
failed: metrics.failed,
|
||||
unsynced: sources.pagination.count === 0 ? 1 : metrics.unsynced,
|
||||
total: sources.pagination.count,
|
||||
label: msg("LDAP Source"),
|
||||
unsynced: objects.pagination.count === 0 ? 1 : metrics.unsynced,
|
||||
total: objects.pagination.count,
|
||||
label: label,
|
||||
};
|
||||
}
|
||||
|
||||
async scimStatus(): Promise<SyncStatus> {
|
||||
const api = new ProvidersApi(DEFAULT_CONFIG);
|
||||
const providers = await api.providersScimList({});
|
||||
const metrics: { [key: string]: number } = {
|
||||
healthy: 0,
|
||||
failed: 0,
|
||||
unsynced: 0,
|
||||
};
|
||||
await Promise.all(
|
||||
providers.results.map(async (element) => {
|
||||
// Each source should have 3 successful tasks, so the worst task overwrites
|
||||
let sourceKey = "healthy";
|
||||
try {
|
||||
const health = await api.providersScimSyncStatusRetrieve({
|
||||
async apiRequest(): Promise<SummarizedSyncStatus[]> {
|
||||
const statuses = [
|
||||
await this.fetchStatus(
|
||||
() => {
|
||||
return new ProvidersApi(DEFAULT_CONFIG).providersScimList();
|
||||
},
|
||||
(element) => {
|
||||
return new ProvidersApi(DEFAULT_CONFIG).providersScimSyncStatusRetrieve({
|
||||
id: element.pk,
|
||||
});
|
||||
health.tasks.forEach((task) => {
|
||||
if (task.status !== SystemTaskStatusEnum.Successful) {
|
||||
sourceKey = "failed";
|
||||
}
|
||||
const now = new Date().getTime();
|
||||
const maxDelta = 3600000; // 1 hour
|
||||
if (!health || now - task.finishTimestamp.getTime() > maxDelta) {
|
||||
sourceKey = "unsynced";
|
||||
}
|
||||
},
|
||||
msg("SCIM Provider"),
|
||||
),
|
||||
await this.fetchStatus(
|
||||
() => {
|
||||
return new ProvidersApi(DEFAULT_CONFIG).providersGoogleWorkspaceList();
|
||||
},
|
||||
(element) => {
|
||||
return new ProvidersApi(
|
||||
DEFAULT_CONFIG,
|
||||
).providersGoogleWorkspaceSyncStatusRetrieve({
|
||||
id: element.pk,
|
||||
});
|
||||
} catch {
|
||||
sourceKey = "unsynced";
|
||||
}
|
||||
metrics[sourceKey] += 1;
|
||||
}),
|
||||
);
|
||||
return {
|
||||
healthy: metrics.healthy,
|
||||
failed: metrics.failed,
|
||||
unsynced: providers.pagination.count === 0 ? 1 : metrics.unsynced,
|
||||
total: providers.pagination.count,
|
||||
label: msg("SCIM Provider"),
|
||||
};
|
||||
},
|
||||
msg("Google Workspace Provider"),
|
||||
),
|
||||
await this.fetchStatus(
|
||||
() => {
|
||||
return new SourcesApi(DEFAULT_CONFIG).sourcesLdapList();
|
||||
},
|
||||
(element) => {
|
||||
return new SourcesApi(DEFAULT_CONFIG).sourcesLdapSyncStatusRetrieve({
|
||||
slug: element.slug,
|
||||
});
|
||||
},
|
||||
msg("LDAP Source"),
|
||||
),
|
||||
];
|
||||
this.centerText = statuses.reduce((total, el) => (total += el.total), 0).toString();
|
||||
return statuses;
|
||||
}
|
||||
|
||||
async apiRequest(): Promise<SyncStatus[]> {
|
||||
const ldapStatus = await this.ldapStatus();
|
||||
const scimStatus = await this.scimStatus();
|
||||
this.centerText = (ldapStatus.total + scimStatus.total).toString();
|
||||
return [ldapStatus, scimStatus];
|
||||
}
|
||||
|
||||
getChartData(data: SyncStatus[]): ChartData {
|
||||
getChartData(data: SummarizedSyncStatus[]): ChartData {
|
||||
return {
|
||||
labels: [msg("Healthy"), msg("Failed"), msg("Unsynced / N/A")],
|
||||
datasets: data.map((d) => {
|
||||
|
@ -0,0 +1,72 @@
|
||||
import { BasePropertyMappingForm } from "@goauthentik/admin/property-mappings/BasePropertyMappingForm";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { docLink } from "@goauthentik/common/global";
|
||||
import "@goauthentik/elements/CodeMirror";
|
||||
import { CodeMirrorMode } from "@goauthentik/elements/CodeMirror";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { TemplateResult, html } from "lit";
|
||||
import { customElement } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import { GoogleProviderMapping, PropertymappingsApi } from "@goauthentik/api";
|
||||
|
||||
@customElement("ak-property-mapping-google-workspace-form")
|
||||
export class PropertyMappingGoogleWorkspaceForm extends BasePropertyMappingForm<GoogleProviderMapping> {
|
||||
loadInstance(pk: string): Promise<GoogleProviderMapping> {
|
||||
return new PropertymappingsApi(
|
||||
DEFAULT_CONFIG,
|
||||
).propertymappingsProviderGoogleWorkspaceRetrieve({
|
||||
pmUuid: pk,
|
||||
});
|
||||
}
|
||||
|
||||
async send(data: GoogleProviderMapping): Promise<GoogleProviderMapping> {
|
||||
if (this.instance) {
|
||||
return new PropertymappingsApi(
|
||||
DEFAULT_CONFIG,
|
||||
).propertymappingsProviderGoogleWorkspaceUpdate({
|
||||
pmUuid: this.instance.pk || "",
|
||||
googleProviderMappingRequest: data,
|
||||
});
|
||||
} else {
|
||||
return new PropertymappingsApi(
|
||||
DEFAULT_CONFIG,
|
||||
).propertymappingsProviderGoogleWorkspaceCreate({
|
||||
googleProviderMappingRequest: data,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
renderForm(): TemplateResult {
|
||||
return html` <ak-form-element-horizontal label=${msg("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=${msg("Expression")}
|
||||
?required=${true}
|
||||
name="expression"
|
||||
>
|
||||
<ak-codemirror
|
||||
mode=${CodeMirrorMode.Python}
|
||||
value="${ifDefined(this.instance?.expression)}"
|
||||
>
|
||||
</ak-codemirror>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Expression using Python.")}
|
||||
<a
|
||||
target="_blank"
|
||||
href="${docLink("/docs/property-mappings/expression?utm_source=authentik")}"
|
||||
>
|
||||
${msg("See documentation for a list of all variables.")}
|
||||
</a>
|
||||
</p>
|
||||
</ak-form-element-horizontal>`;
|
||||
}
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
import "@goauthentik/admin/property-mappings/PropertyMappingGoogleWorkspaceForm";
|
||||
import "@goauthentik/admin/property-mappings/PropertyMappingLDAPForm";
|
||||
import "@goauthentik/admin/property-mappings/PropertyMappingNotification";
|
||||
import "@goauthentik/admin/property-mappings/PropertyMappingRACForm";
|
||||
|
@ -7,6 +7,7 @@ import "@goauthentik/admin/providers/rac/RACProviderForm";
|
||||
import "@goauthentik/admin/providers/radius/RadiusProviderForm";
|
||||
import "@goauthentik/admin/providers/saml/SAMLProviderForm";
|
||||
import "@goauthentik/admin/providers/scim/SCIMProviderForm";
|
||||
import "@goauthentik/authentik/admin/providers/google_workspace/GoogleWorkspaceProviderForm";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { uiConfig } from "@goauthentik/common/ui/config";
|
||||
import "@goauthentik/elements/buttons/SpinnerButton";
|
||||
|
@ -5,6 +5,7 @@ import "@goauthentik/admin/providers/rac/RACProviderViewPage";
|
||||
import "@goauthentik/admin/providers/radius/RadiusProviderViewPage";
|
||||
import "@goauthentik/admin/providers/saml/SAMLProviderViewPage";
|
||||
import "@goauthentik/admin/providers/scim/SCIMProviderViewPage";
|
||||
import "@goauthentik/authentik/admin/providers/google_workspace/GoogleWorkspaceProviderViewPage";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
import "@goauthentik/elements/EmptyState";
|
||||
@ -70,6 +71,10 @@ export class ProviderViewPage extends AKElement {
|
||||
return html`<ak-provider-rac-view
|
||||
providerID=${ifDefined(this.provider.pk)}
|
||||
></ak-provider-rac-view>`;
|
||||
case "ak-provider-google-workspace-form":
|
||||
return html`<ak-provider-google-workspace-view
|
||||
providerID=${ifDefined(this.provider.pk)}
|
||||
></ak-provider-google-workspace-view>`;
|
||||
default:
|
||||
return html`<p>Invalid provider type ${this.provider?.component}</p>`;
|
||||
}
|
||||
|
@ -0,0 +1,291 @@
|
||||
import { BaseProviderForm } from "@goauthentik/admin/providers/BaseProviderForm";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { first } from "@goauthentik/common/utils";
|
||||
import "@goauthentik/elements/CodeMirror";
|
||||
import { CodeMirrorMode } from "@goauthentik/elements/CodeMirror";
|
||||
import "@goauthentik/elements/forms/FormGroup";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
import "@goauthentik/elements/forms/Radio";
|
||||
import "@goauthentik/elements/forms/SearchSelect";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { TemplateResult, html } from "lit";
|
||||
import { customElement } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import {
|
||||
CoreApi,
|
||||
CoreGroupsListRequest,
|
||||
GoogleProvider,
|
||||
GoogleWorkspaceDeleteAction,
|
||||
Group,
|
||||
PaginatedGoogleProviderMappingList,
|
||||
PropertymappingsApi,
|
||||
ProvidersApi,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
@customElement("ak-provider-google-workspace-form")
|
||||
export class GoogleWorkspaceProviderFormPage extends BaseProviderForm<GoogleProvider> {
|
||||
loadInstance(pk: number): Promise<GoogleProvider> {
|
||||
return new ProvidersApi(DEFAULT_CONFIG).providersGoogleWorkspaceRetrieve({
|
||||
id: pk,
|
||||
});
|
||||
}
|
||||
|
||||
async load(): Promise<void> {
|
||||
this.propertyMappings = await new PropertymappingsApi(
|
||||
DEFAULT_CONFIG,
|
||||
).propertymappingsProviderGoogleWorkspaceList({
|
||||
ordering: "managed",
|
||||
});
|
||||
}
|
||||
|
||||
propertyMappings?: PaginatedGoogleProviderMappingList;
|
||||
|
||||
async send(data: GoogleProvider): Promise<GoogleProvider> {
|
||||
if (this.instance) {
|
||||
return new ProvidersApi(DEFAULT_CONFIG).providersGoogleWorkspaceUpdate({
|
||||
id: this.instance.pk || 0,
|
||||
googleProviderRequest: data,
|
||||
});
|
||||
} else {
|
||||
return new ProvidersApi(DEFAULT_CONFIG).providersGoogleWorkspaceCreate({
|
||||
googleProviderRequest: data,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
renderForm(): TemplateResult {
|
||||
return html` <ak-form-element-horizontal label=${msg("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-group .expanded=${true}>
|
||||
<span slot="header"> ${msg("Protocol settings")} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Credentials")}
|
||||
?required=${true}
|
||||
name="credentials"
|
||||
>
|
||||
<ak-codemirror
|
||||
mode=${CodeMirrorMode.JavaScript}
|
||||
.value="${first(this.instance?.credentials, {})}"
|
||||
></ak-codemirror>
|
||||
<p class="pf-c-form__helper-text">${msg("TODO")}</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Delegated Subject")}
|
||||
?required=${true}
|
||||
name="delegatedSubject"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
value="${first(this.instance?.delegatedSubject, "")}"
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
/>
|
||||
<p class="pf-c-form__helper-text">${msg("TODO")}</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Default group email domain")}
|
||||
?required=${true}
|
||||
name="defaultGroupEmailDomain"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
value="${first(this.instance?.defaultGroupEmailDomain, "")}"
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
/>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"Default domain that is used to generate a group's email address. Can be customized using property mappings.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-radio-input
|
||||
name="userDeleteAction"
|
||||
label=${msg("User deletion action")}
|
||||
required
|
||||
.options=${[
|
||||
{
|
||||
label: msg("Delete"),
|
||||
value: GoogleWorkspaceDeleteAction.Delete,
|
||||
default: true,
|
||||
description: html`${msg("User is deleted")}`,
|
||||
},
|
||||
{
|
||||
label: msg("Suspend"),
|
||||
value: GoogleWorkspaceDeleteAction.Suspend,
|
||||
description: html`${msg(
|
||||
"User is suspended, and connection to user in authentik is removed.",
|
||||
)}`,
|
||||
},
|
||||
{
|
||||
label: msg("Do Nothing"),
|
||||
value: GoogleWorkspaceDeleteAction.DoNothing,
|
||||
description: html`${msg(
|
||||
"The connection is removed but the user is not modified",
|
||||
)}`,
|
||||
},
|
||||
]}
|
||||
.value=${this.instance?.userDeleteAction}
|
||||
help=${msg("Determines what authentik will do when a User is deleted.")}
|
||||
>
|
||||
</ak-radio-input>
|
||||
<ak-radio-input
|
||||
name="groupDeleteAction"
|
||||
label=${msg("Group deletion action")}
|
||||
required
|
||||
.options=${[
|
||||
{
|
||||
label: msg("Delete"),
|
||||
value: GoogleWorkspaceDeleteAction.Delete,
|
||||
default: true,
|
||||
description: html`${msg("Group is deleted")}`,
|
||||
},
|
||||
{
|
||||
label: msg("Do Nothing"),
|
||||
value: GoogleWorkspaceDeleteAction.DoNothing,
|
||||
description: html`${msg(
|
||||
"The connection is removed but the group is not modified",
|
||||
)}`,
|
||||
},
|
||||
]}
|
||||
.value=${this.instance?.groupDeleteAction}
|
||||
help=${msg("Determines what authentik will do when a Group is deleted.")}
|
||||
>
|
||||
</ak-radio-input>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
<ak-form-group ?expanded=${true}>
|
||||
<span slot="header">${msg("User filtering")}</span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-form-element-horizontal name="excludeUsersServiceAccount">
|
||||
<label class="pf-c-switch">
|
||||
<input
|
||||
class="pf-c-switch__input"
|
||||
type="checkbox"
|
||||
?checked=${first(this.instance?.excludeUsersServiceAccount, true)}
|
||||
/>
|
||||
<span class="pf-c-switch__toggle">
|
||||
<span class="pf-c-switch__toggle-icon">
|
||||
<i class="fas fa-check" aria-hidden="true"></i>
|
||||
</span>
|
||||
</span>
|
||||
<span class="pf-c-switch__label"
|
||||
>${msg("Exclude service accounts")}</span
|
||||
>
|
||||
</label>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal label=${msg("Group")} name="filterGroup">
|
||||
<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?.filterGroup;
|
||||
}}
|
||||
?blankable=${true}
|
||||
>
|
||||
</ak-search-select>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Only sync users within the selected group.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
<ak-form-group ?expanded=${true}>
|
||||
<span slot="header"> ${msg("Attribute mapping")} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("User Property Mappings")}
|
||||
name="propertyMappings"
|
||||
>
|
||||
<select class="pf-c-form-control" multiple>
|
||||
${this.propertyMappings?.results.map((mapping) => {
|
||||
let selected = false;
|
||||
if (!this.instance?.propertyMappings) {
|
||||
selected =
|
||||
mapping.managed ===
|
||||
"goauthentik.io/providers/google_workspace/user" ||
|
||||
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>`;
|
||||
})}
|
||||
</select>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Property mappings used to user mapping.")}
|
||||
</p>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Hold control/command to select multiple items.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Group Property Mappings")}
|
||||
name="propertyMappingsGroup"
|
||||
>
|
||||
<select class="pf-c-form-control" multiple>
|
||||
${this.propertyMappings?.results.map((mapping) => {
|
||||
let selected = false;
|
||||
if (!this.instance?.propertyMappingsGroup) {
|
||||
selected =
|
||||
mapping.managed ===
|
||||
"goauthentik.io/providers/google_workspace/group";
|
||||
} else {
|
||||
selected = Array.from(
|
||||
this.instance?.propertyMappingsGroup,
|
||||
).some((su) => {
|
||||
return su == mapping.pk;
|
||||
});
|
||||
}
|
||||
return html`<option
|
||||
value=${ifDefined(mapping.pk)}
|
||||
?selected=${selected}
|
||||
>
|
||||
${mapping.name}
|
||||
</option>`;
|
||||
})}
|
||||
</select>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Property mappings used to group creation.")}
|
||||
</p>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Hold control/command to select multiple items.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
</ak-form-group>`;
|
||||
}
|
||||
}
|
@ -0,0 +1,242 @@
|
||||
import "@goauthentik/authentik/admin/providers/google_workspace/GoogleWorkspaceProviderForm";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { EVENT_REFRESH } from "@goauthentik/common/constants";
|
||||
import "@goauthentik/components/events/ObjectChangelog";
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
import "@goauthentik/elements/Markdown";
|
||||
import "@goauthentik/elements/Tabs";
|
||||
import "@goauthentik/elements/buttons/ActionButton";
|
||||
import "@goauthentik/elements/buttons/ModalButton";
|
||||
import "@goauthentik/elements/events/LogViewer";
|
||||
import "@goauthentik/elements/rbac/ObjectPermissionsPage";
|
||||
|
||||
import { msg, str } from "@lit/localize";
|
||||
import { CSSResult, PropertyValues, TemplateResult, 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 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 PFStack from "@patternfly/patternfly/layouts/Stack/stack.css";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
|
||||
import {
|
||||
GoogleProvider,
|
||||
ProvidersApi,
|
||||
RbacPermissionsAssignedByUsersListModelEnum,
|
||||
SyncStatus,
|
||||
SystemTaskStatusEnum,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
@customElement("ak-provider-google-workspace-view")
|
||||
export class GoogleWorkspaceProviderViewPage extends AKElement {
|
||||
@property({ type: Number })
|
||||
providerID?: number;
|
||||
|
||||
@state()
|
||||
provider?: GoogleProvider;
|
||||
|
||||
@state()
|
||||
syncState?: SyncStatus;
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
PFBase,
|
||||
PFButton,
|
||||
PFBanner,
|
||||
PFForm,
|
||||
PFFormControl,
|
||||
PFStack,
|
||||
PFList,
|
||||
PFGrid,
|
||||
PFPage,
|
||||
PFContent,
|
||||
PFCard,
|
||||
PFDescriptionList,
|
||||
];
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.addEventListener(EVENT_REFRESH, () => {
|
||||
if (!this.provider?.pk) return;
|
||||
this.providerID = this.provider?.pk;
|
||||
});
|
||||
}
|
||||
|
||||
fetchProvider(id: number) {
|
||||
new ProvidersApi(DEFAULT_CONFIG)
|
||||
.providersGoogleWorkspaceRetrieve({ id })
|
||||
.then((prov) => (this.provider = prov));
|
||||
}
|
||||
|
||||
willUpdate(changedProperties: PropertyValues<this>) {
|
||||
if (changedProperties.has("providerID") && this.providerID) {
|
||||
this.fetchProvider(this.providerID);
|
||||
}
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
if (!this.provider) {
|
||||
return html``;
|
||||
}
|
||||
return html` <ak-tabs>
|
||||
<section
|
||||
slot="page-overview"
|
||||
data-tab-title="${msg("Overview")}"
|
||||
@activate=${() => {
|
||||
new ProvidersApi(DEFAULT_CONFIG)
|
||||
.providersGoogleWorkspaceSyncStatusRetrieve({
|
||||
id: this.provider?.pk || 0,
|
||||
})
|
||||
.then((state) => {
|
||||
this.syncState = state;
|
||||
})
|
||||
.catch(() => {
|
||||
this.syncState = undefined;
|
||||
});
|
||||
}}
|
||||
>
|
||||
${this.renderTabOverview()}
|
||||
</section>
|
||||
<section
|
||||
slot="page-changelog"
|
||||
data-tab-title="${msg("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-rbac-object-permission-page
|
||||
slot="page-permissions"
|
||||
data-tab-title="${msg("Permissions")}"
|
||||
model=${RbacPermissionsAssignedByUsersListModelEnum.ProvidersGoogleWorkspaceGoogleworkspaceprovider}
|
||||
objectPk=${this.provider.pk}
|
||||
></ak-rbac-object-permission-page>
|
||||
</ak-tabs>`;
|
||||
}
|
||||
|
||||
renderSyncStatus(): TemplateResult {
|
||||
if (!this.syncState) {
|
||||
return html`${msg("No sync status.")}`;
|
||||
}
|
||||
if (this.syncState.isRunning) {
|
||||
return html`${msg("Sync currently running.")}`;
|
||||
}
|
||||
if (this.syncState.tasks.length < 1) {
|
||||
return html`${msg("Not synced yet.")}`;
|
||||
}
|
||||
return html`
|
||||
<ul class="pf-c-list">
|
||||
${this.syncState.tasks.map((task) => {
|
||||
let header = "";
|
||||
if (task.status === SystemTaskStatusEnum.Warning) {
|
||||
header = msg("Task finished with warnings");
|
||||
} else if (task.status === SystemTaskStatusEnum.Error) {
|
||||
header = msg("Task finished with errors");
|
||||
} else {
|
||||
header = msg(str`Last sync: ${task.finishTimestamp.toLocaleString()}`);
|
||||
}
|
||||
return html`<li>
|
||||
<p>${task.name}</p>
|
||||
<ul class="pf-c-list">
|
||||
<li>${header}</li>
|
||||
<ak-log-viewer .logs=${task?.messages}></ak-log-viewer>
|
||||
</ul>
|
||||
</li> `;
|
||||
})}
|
||||
</ul>
|
||||
`;
|
||||
}
|
||||
|
||||
renderTabOverview(): TemplateResult {
|
||||
if (!this.provider) {
|
||||
return html``;
|
||||
}
|
||||
return html`<div slot="header" class="pf-c-banner pf-m-info">
|
||||
${msg("Google Workspace Provider is in preview.")}
|
||||
<a href="mailto:hello+feature/gws@goauthentik.io">${msg("Send us feedback!")}</a>
|
||||
</div>
|
||||
${!this.provider?.assignedBackchannelApplicationName
|
||||
? html`<div slot="header" class="pf-c-banner pf-m-warning">
|
||||
${msg(
|
||||
"Warning: Provider is not assigned to an application as backchannel provider.",
|
||||
)}
|
||||
</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-m-12-col pf-l-stack__item">
|
||||
<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">${msg("Name")}</span>
|
||||
</dt>
|
||||
<dd class="pf-c-description-list__description">
|
||||
<div class="pf-c-description-list__text">
|
||||
${this.provider.name}
|
||||
</div>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
<div class="pf-c-card__footer">
|
||||
<ak-forms-modal>
|
||||
<span slot="submit"> ${msg("Update")} </span>
|
||||
<span slot="header"> ${msg("Update Google Provider")} </span>
|
||||
<ak-provider-google-workspace-form
|
||||
slot="form"
|
||||
.instancePk=${this.provider.pk}
|
||||
>
|
||||
</ak-provider-google-workspace-form>
|
||||
<button slot="trigger" class="pf-c-button pf-m-primary">
|
||||
${msg("Edit")}
|
||||
</button>
|
||||
</ak-forms-modal>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pf-c-card pf-l-grid__item pf-m-12-col pf-l-stack__item">
|
||||
<div class="pf-c-card__title">
|
||||
<p>${msg("Sync status")}</p>
|
||||
</div>
|
||||
<div class="pf-c-card__body">${this.renderSyncStatus()}</div>
|
||||
<div class="pf-c-card__footer">
|
||||
<ak-action-button
|
||||
class="pf-m-secondary"
|
||||
.apiRequest=${() => {
|
||||
return new ProvidersApi(DEFAULT_CONFIG)
|
||||
.providersGoogleWorkspacePartialUpdate({
|
||||
id: this.provider?.pk || 0,
|
||||
patchedGoogleProviderRequest: this.provider,
|
||||
})
|
||||
.then(() => {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent(EVENT_REFRESH, {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
}}
|
||||
>
|
||||
${msg("Run sync again")}
|
||||
</ak-action-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
}
|
@ -129,7 +129,7 @@ export class RACProviderViewPage extends AKElement {
|
||||
if (!this.provider) {
|
||||
return html``;
|
||||
}
|
||||
return html` <div slot="header" class="pf-c-banner pf-m-info">
|
||||
return html`<div slot="header" class="pf-c-banner pf-m-info">
|
||||
${msg("RAC is in preview.")}
|
||||
<a href="mailto:hello+feature/rac@goauthentik.io">${msg("Send us feedback!")}</a>
|
||||
</div>
|
||||
|
@ -32,7 +32,7 @@ import {
|
||||
ProvidersApi,
|
||||
RbacPermissionsAssignedByUsersListModelEnum,
|
||||
SCIMProvider,
|
||||
SCIMSyncStatus,
|
||||
SyncStatus,
|
||||
SystemTaskStatusEnum,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
@ -45,7 +45,7 @@ export class SCIMProviderViewPage extends AKElement {
|
||||
provider?: SCIMProvider;
|
||||
|
||||
@state()
|
||||
syncState?: SCIMSyncStatus;
|
||||
syncState?: SyncStatus;
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
|
@ -26,9 +26,9 @@ import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
|
||||
import {
|
||||
LDAPSource,
|
||||
LDAPSyncStatus,
|
||||
RbacPermissionsAssignedByUsersListModelEnum,
|
||||
SourcesApi,
|
||||
SyncStatus,
|
||||
SystemTaskStatusEnum,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
@ -49,7 +49,7 @@ export class LDAPSourceViewPage extends AKElement {
|
||||
source!: LDAPSource;
|
||||
|
||||
@state()
|
||||
syncState?: LDAPSyncStatus;
|
||||
syncState?: SyncStatus;
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [PFBase, PFPage, PFButton, PFGrid, PFContent, PFCard, PFDescriptionList, PFList];
|
||||
|
@ -5,7 +5,7 @@ import { WizardPage } from "@goauthentik/elements/wizard/WizardPage";
|
||||
import { msg } from "@lit/localize";
|
||||
import { customElement } from "@lit/reactive-element/decorators/custom-element.js";
|
||||
import { property } from "@lit/reactive-element/decorators/property.js";
|
||||
import { CSSResult, TemplateResult, html } from "lit";
|
||||
import { CSSResult, TemplateResult, css, html } from "lit";
|
||||
import { state } from "lit/decorators.js";
|
||||
|
||||
import PFWizard from "@patternfly/patternfly/components/Wizard/wizard.css";
|
||||
@ -36,7 +36,14 @@ export class Wizard extends ModalButton {
|
||||
isValid = false;
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return super.styles.concat(PFWizard);
|
||||
return super.styles.concat(
|
||||
PFWizard,
|
||||
css`
|
||||
.pf-c-modal-box {
|
||||
height: 75%;
|
||||
}
|
||||
`,
|
||||
);
|
||||
}
|
||||
|
||||
@state()
|
||||
|
Reference in New Issue
Block a user