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:
Jens L
2024-05-07 19:52:20 +02:00
committed by GitHub
parent 18b4b2d7b2
commit aeb1b450eb
84 changed files with 4307 additions and 619 deletions

View File

@ -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) => {

View File

@ -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) => {

View File

@ -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>`;
}
}

View File

@ -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";

View File

@ -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";

View File

@ -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>`;
}

View File

@ -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>`;
}
}

View File

@ -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>`;
}
}

View File

@ -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>

View File

@ -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 [

View File

@ -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];

View File

@ -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()