mirror of
https://github.com/docmost/docmost.git
synced 2026-05-22 18:22:42 +08:00
feat(EE): implement SSO group sync for SAML and OIDC (#1452)
* feat: implement SSO group synchronization for SAML and OIDC - Add group_sync column to auth_providers table - Extract groups from SAML attributes (memberOf, groups, roles) - Extract groups from OIDC claims (groups, roles) - Implement case-insensitive group matching with auto-creation - Sync user groups on each SSO login - Ensure only one provider can have group sync enabled at a time - Add group sync toggle to SAML and OIDC configuration forms * rename column
This commit is contained in:
@@ -16,6 +16,7 @@ const ssoSchema = z.object({
|
|||||||
oidcClientSecret: z.string().min(1, "Client secret is required"),
|
oidcClientSecret: z.string().min(1, "Client secret is required"),
|
||||||
isEnabled: z.boolean(),
|
isEnabled: z.boolean(),
|
||||||
allowSignup: z.boolean(),
|
allowSignup: z.boolean(),
|
||||||
|
groupSync: z.boolean(),
|
||||||
});
|
});
|
||||||
|
|
||||||
type SSOFormValues = z.infer<typeof ssoSchema>;
|
type SSOFormValues = z.infer<typeof ssoSchema>;
|
||||||
@@ -36,6 +37,7 @@ export function SsoOIDCForm({ provider, onClose }: SsoFormProps) {
|
|||||||
oidcClientSecret: provider.oidcClientSecret || "",
|
oidcClientSecret: provider.oidcClientSecret || "",
|
||||||
isEnabled: provider.isEnabled,
|
isEnabled: provider.isEnabled,
|
||||||
allowSignup: provider.allowSignup,
|
allowSignup: provider.allowSignup,
|
||||||
|
groupSync: provider.groupSync || false,
|
||||||
},
|
},
|
||||||
validate: zodResolver(ssoSchema),
|
validate: zodResolver(ssoSchema),
|
||||||
});
|
});
|
||||||
@@ -67,6 +69,9 @@ export function SsoOIDCForm({ provider, onClose }: SsoFormProps) {
|
|||||||
if (form.isDirty("allowSignup")) {
|
if (form.isDirty("allowSignup")) {
|
||||||
ssoData.allowSignup = values.allowSignup;
|
ssoData.allowSignup = values.allowSignup;
|
||||||
}
|
}
|
||||||
|
if (form.isDirty("groupSync")) {
|
||||||
|
ssoData.groupSync = values.groupSync;
|
||||||
|
}
|
||||||
|
|
||||||
await updateSsoProviderMutation.mutateAsync(ssoData);
|
await updateSsoProviderMutation.mutateAsync(ssoData);
|
||||||
form.resetDirty();
|
form.resetDirty();
|
||||||
@@ -119,6 +124,15 @@ export function SsoOIDCForm({ provider, onClose }: SsoFormProps) {
|
|||||||
/>
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
|
<Group justify="space-between">
|
||||||
|
<div>{t("Group sync")}</div>
|
||||||
|
<Switch
|
||||||
|
className={classes.switch}
|
||||||
|
checked={form.values.groupSync}
|
||||||
|
{...form.getInputProps("groupSync")}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
|
||||||
<Group justify="space-between">
|
<Group justify="space-between">
|
||||||
<div>{t("Enabled")}</div>
|
<div>{t("Enabled")}</div>
|
||||||
<Switch
|
<Switch
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ const ssoSchema = z.object({
|
|||||||
samlCertificate: z.string().min(1, "SAML Idp Certificate is required"),
|
samlCertificate: z.string().min(1, "SAML Idp Certificate is required"),
|
||||||
isEnabled: z.boolean(),
|
isEnabled: z.boolean(),
|
||||||
allowSignup: z.boolean(),
|
allowSignup: z.boolean(),
|
||||||
|
groupSync: z.boolean(),
|
||||||
});
|
});
|
||||||
|
|
||||||
type SSOFormValues = z.infer<typeof ssoSchema>;
|
type SSOFormValues = z.infer<typeof ssoSchema>;
|
||||||
@@ -45,6 +46,7 @@ export function SsoSamlForm({ provider, onClose }: SsoFormProps) {
|
|||||||
samlCertificate: provider.samlCertificate || "",
|
samlCertificate: provider.samlCertificate || "",
|
||||||
isEnabled: provider.isEnabled,
|
isEnabled: provider.isEnabled,
|
||||||
allowSignup: provider.allowSignup,
|
allowSignup: provider.allowSignup,
|
||||||
|
groupSync: provider.groupSync || false,
|
||||||
},
|
},
|
||||||
validate: zodResolver(ssoSchema),
|
validate: zodResolver(ssoSchema),
|
||||||
});
|
});
|
||||||
@@ -75,6 +77,9 @@ export function SsoSamlForm({ provider, onClose }: SsoFormProps) {
|
|||||||
if (form.isDirty("allowSignup")) {
|
if (form.isDirty("allowSignup")) {
|
||||||
ssoData.allowSignup = values.allowSignup;
|
ssoData.allowSignup = values.allowSignup;
|
||||||
}
|
}
|
||||||
|
if (form.isDirty("groupSync")) {
|
||||||
|
ssoData.groupSync = values.groupSync;
|
||||||
|
}
|
||||||
|
|
||||||
await updateSsoProviderMutation.mutateAsync(ssoData);
|
await updateSsoProviderMutation.mutateAsync(ssoData);
|
||||||
form.resetDirty();
|
form.resetDirty();
|
||||||
@@ -132,6 +137,15 @@ export function SsoSamlForm({ provider, onClose }: SsoFormProps) {
|
|||||||
/>
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
|
<Group justify="space-between">
|
||||||
|
<div>{t("Group sync")}</div>
|
||||||
|
<Switch
|
||||||
|
className={classes.switch}
|
||||||
|
checked={form.values.groupSync}
|
||||||
|
{...form.getInputProps("groupSync")}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
|
||||||
<Group justify="space-between">
|
<Group justify="space-between">
|
||||||
<div>{t("Enabled")}</div>
|
<div>{t("Enabled")}</div>
|
||||||
<Switch
|
<Switch
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export interface IAuthProvider {
|
|||||||
oidcClientSecret: string;
|
oidcClientSecret: string;
|
||||||
allowSignup: boolean;
|
allowSignup: boolean;
|
||||||
isEnabled: boolean;
|
isEnabled: boolean;
|
||||||
|
groupSync: boolean;
|
||||||
creatorId: string;
|
creatorId: string;
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
|
|||||||
+2
-4
@@ -3,15 +3,13 @@ import { type Kysely } from 'kysely';
|
|||||||
export async function up(db: Kysely<any>): Promise<void> {
|
export async function up(db: Kysely<any>): Promise<void> {
|
||||||
await db.schema
|
await db.schema
|
||||||
.alterTable('auth_providers')
|
.alterTable('auth_providers')
|
||||||
.addColumn('is_group_sync_enabled', 'boolean', (col) =>
|
.addColumn('group_sync', 'boolean', (col) => col.defaultTo(false).notNull())
|
||||||
col.defaultTo(false).notNull(),
|
|
||||||
)
|
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function down(db: Kysely<any>): Promise<void> {
|
export async function down(db: Kysely<any>): Promise<void> {
|
||||||
await db.schema
|
await db.schema
|
||||||
.alterTable('auth_providers')
|
.alterTable('auth_providers')
|
||||||
.dropColumn('is_group_sync_enabled')
|
.dropColumn('group_sync')
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
+1
@@ -62,6 +62,7 @@ export interface AuthProviders {
|
|||||||
deletedAt: Timestamp | null;
|
deletedAt: Timestamp | null;
|
||||||
id: Generated<string>;
|
id: Generated<string>;
|
||||||
isEnabled: Generated<boolean>;
|
isEnabled: Generated<boolean>;
|
||||||
|
groupSync: Generated<boolean>;
|
||||||
name: string;
|
name: string;
|
||||||
oidcClientId: string | null;
|
oidcClientId: string | null;
|
||||||
oidcClientSecret: string | null;
|
oidcClientSecret: string | null;
|
||||||
|
|||||||
+1
-1
Submodule apps/server/src/ee updated: fbc01d808f...10519eeaa2
Reference in New Issue
Block a user