mirror of
https://github.com/fosrl/pangolin.git
synced 2026-03-29 18:05:37 +00:00
basic idp mapping builder
This commit is contained in:
@@ -47,6 +47,14 @@ import { ListRolesResponse } from "@server/routers/role";
|
||||
import AutoProvisionConfigWidget from "@app/components/AutoProvisionConfigWidget";
|
||||
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
|
||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
import {
|
||||
compileRoleMappingExpression,
|
||||
createMappingBuilderRule,
|
||||
detectRoleMappingConfig,
|
||||
ensureMappingBuilderRuleIds,
|
||||
MappingBuilderRule,
|
||||
RoleMappingMode
|
||||
} from "@app/lib/idpRoleMapping";
|
||||
|
||||
export default function GeneralPage() {
|
||||
const { env } = useEnvContext();
|
||||
@@ -56,9 +64,15 @@ export default function GeneralPage() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [initialLoading, setInitialLoading] = useState(true);
|
||||
const [roles, setRoles] = useState<{ roleId: number; name: string }[]>([]);
|
||||
const [roleMappingMode, setRoleMappingMode] = useState<
|
||||
"role" | "expression"
|
||||
>("role");
|
||||
const [roleMappingMode, setRoleMappingMode] =
|
||||
useState<RoleMappingMode>("fixedRoles");
|
||||
const [fixedRoleNames, setFixedRoleNames] = useState<string[]>([]);
|
||||
const [mappingBuilderClaimPath, setMappingBuilderClaimPath] =
|
||||
useState("groups");
|
||||
const [mappingBuilderRules, setMappingBuilderRules] = useState<
|
||||
MappingBuilderRule[]
|
||||
>([createMappingBuilderRule()]);
|
||||
const [rawRoleExpression, setRawRoleExpression] = useState("");
|
||||
const [variant, setVariant] = useState<"oidc" | "google" | "azure">("oidc");
|
||||
|
||||
const dashboardRedirectUrl = `${env.app.dashboardUrl}/auth/idp/${idpId}/oidc/callback`;
|
||||
@@ -190,34 +204,8 @@ export default function GeneralPage() {
|
||||
// Set the variant
|
||||
setVariant(idpVariant as "oidc" | "google" | "azure");
|
||||
|
||||
// Check if roleMapping matches the basic pattern '{role name}' (simple single role)
|
||||
// This should NOT match complex expressions like 'Admin' || 'Member'
|
||||
const isBasicRolePattern =
|
||||
roleMapping &&
|
||||
typeof roleMapping === "string" &&
|
||||
/^'[^']+'$/.test(roleMapping);
|
||||
|
||||
// Determine if roleMapping is a number (roleId) or matches basic pattern
|
||||
const isRoleId =
|
||||
!isNaN(Number(roleMapping)) && roleMapping !== "";
|
||||
const isRoleName = isBasicRolePattern;
|
||||
|
||||
// Extract role name from basic pattern for matching
|
||||
let extractedRoleName = null;
|
||||
if (isRoleName) {
|
||||
extractedRoleName = roleMapping.slice(1, -1); // Remove quotes
|
||||
}
|
||||
|
||||
// Try to find matching role by name if we have a basic pattern
|
||||
let matchingRoleId = undefined;
|
||||
if (extractedRoleName && availableRoles.length > 0) {
|
||||
const matchingRole = availableRoles.find(
|
||||
(role) => role.name === extractedRoleName
|
||||
);
|
||||
if (matchingRole) {
|
||||
matchingRoleId = matchingRole.roleId;
|
||||
}
|
||||
}
|
||||
const detectedRoleMappingConfig =
|
||||
detectRoleMappingConfig(roleMapping);
|
||||
|
||||
// Extract tenant ID from Azure URLs if present
|
||||
let tenantId = "";
|
||||
@@ -238,9 +226,7 @@ export default function GeneralPage() {
|
||||
clientSecret: data.idpOidcConfig.clientSecret,
|
||||
autoProvision: data.idp.autoProvision,
|
||||
roleMapping: roleMapping || null,
|
||||
roleId: isRoleId
|
||||
? Number(roleMapping)
|
||||
: matchingRoleId || null
|
||||
roleId: null
|
||||
};
|
||||
|
||||
// Add variant-specific fields
|
||||
@@ -259,10 +245,18 @@ export default function GeneralPage() {
|
||||
|
||||
form.reset(formData);
|
||||
|
||||
// Set the role mapping mode based on the data
|
||||
// Default to "expression" unless it's a simple roleId or basic '{role name}' pattern
|
||||
setRoleMappingMode(
|
||||
matchingRoleId && isRoleName ? "role" : "expression"
|
||||
setRoleMappingMode(detectedRoleMappingConfig.mode);
|
||||
setFixedRoleNames(detectedRoleMappingConfig.fixedRoleNames);
|
||||
setMappingBuilderClaimPath(
|
||||
detectedRoleMappingConfig.mappingBuilder.claimPath
|
||||
);
|
||||
setMappingBuilderRules(
|
||||
ensureMappingBuilderRuleIds(
|
||||
detectedRoleMappingConfig.mappingBuilder.rules
|
||||
)
|
||||
);
|
||||
setRawRoleExpression(
|
||||
detectedRoleMappingConfig.rawExpression
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -327,7 +321,26 @@ export default function GeneralPage() {
|
||||
return;
|
||||
}
|
||||
|
||||
const roleName = roles.find((r) => r.roleId === data.roleId)?.name;
|
||||
const roleMappingExpression = compileRoleMappingExpression({
|
||||
mode: roleMappingMode,
|
||||
fixedRoleNames,
|
||||
mappingBuilder: {
|
||||
claimPath: mappingBuilderClaimPath,
|
||||
rules: mappingBuilderRules
|
||||
},
|
||||
rawExpression: rawRoleExpression
|
||||
});
|
||||
|
||||
if (data.autoProvision && !roleMappingExpression) {
|
||||
toast({
|
||||
title: t("error"),
|
||||
description:
|
||||
"A role mapping is required when auto-provisioning is enabled.",
|
||||
variant: "destructive"
|
||||
});
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Build payload based on variant
|
||||
let payload: any = {
|
||||
@@ -335,10 +348,7 @@ export default function GeneralPage() {
|
||||
clientId: data.clientId,
|
||||
clientSecret: data.clientSecret,
|
||||
autoProvision: data.autoProvision,
|
||||
roleMapping:
|
||||
roleMappingMode === "role"
|
||||
? `'${roleName}'`
|
||||
: data.roleMapping || ""
|
||||
roleMapping: roleMappingExpression
|
||||
};
|
||||
|
||||
// Add variant-specific fields
|
||||
@@ -497,42 +507,43 @@ export default function GeneralPage() {
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
<SettingsSectionForm>
|
||||
<PaidFeaturesAlert
|
||||
tiers={tierMatrix.autoProvisioning}
|
||||
/>
|
||||
<PaidFeaturesAlert
|
||||
tiers={tierMatrix.autoProvisioning}
|
||||
/>
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="space-y-4"
|
||||
id="general-settings-form"
|
||||
>
|
||||
<AutoProvisionConfigWidget
|
||||
control={form.control}
|
||||
autoProvision={form.watch(
|
||||
"autoProvision"
|
||||
)}
|
||||
onAutoProvisionChange={(checked) => {
|
||||
form.setValue(
|
||||
"autoProvision",
|
||||
checked
|
||||
);
|
||||
}}
|
||||
roleMappingMode={roleMappingMode}
|
||||
onRoleMappingModeChange={(data) => {
|
||||
setRoleMappingMode(data);
|
||||
// Clear roleId and roleMapping when mode changes
|
||||
form.setValue("roleId", null);
|
||||
form.setValue("roleMapping", null);
|
||||
}}
|
||||
roles={roles}
|
||||
roleIdFieldName="roleId"
|
||||
roleMappingFieldName="roleMapping"
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
</SettingsSectionForm>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="space-y-4"
|
||||
id="general-settings-form"
|
||||
>
|
||||
<AutoProvisionConfigWidget
|
||||
autoProvision={form.watch("autoProvision")}
|
||||
onAutoProvisionChange={(checked) => {
|
||||
form.setValue("autoProvision", checked);
|
||||
}}
|
||||
roleMappingMode={roleMappingMode}
|
||||
onRoleMappingModeChange={(data) => {
|
||||
setRoleMappingMode(data);
|
||||
}}
|
||||
roles={roles}
|
||||
fixedRoleNames={fixedRoleNames}
|
||||
onFixedRoleNamesChange={setFixedRoleNames}
|
||||
mappingBuilderClaimPath={
|
||||
mappingBuilderClaimPath
|
||||
}
|
||||
onMappingBuilderClaimPathChange={
|
||||
setMappingBuilderClaimPath
|
||||
}
|
||||
mappingBuilderRules={mappingBuilderRules}
|
||||
onMappingBuilderRulesChange={
|
||||
setMappingBuilderRules
|
||||
}
|
||||
rawExpression={rawRoleExpression}
|
||||
onRawExpressionChange={setRawRoleExpression}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
</SettingsSectionBody>
|
||||
</SettingsSection>
|
||||
|
||||
|
||||
@@ -42,6 +42,12 @@ import { useParams, useRouter } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
compileRoleMappingExpression,
|
||||
createMappingBuilderRule,
|
||||
MappingBuilderRule,
|
||||
RoleMappingMode
|
||||
} from "@app/lib/idpRoleMapping";
|
||||
|
||||
export default function Page() {
|
||||
const { env } = useEnvContext();
|
||||
@@ -49,9 +55,15 @@ export default function Page() {
|
||||
const router = useRouter();
|
||||
const [createLoading, setCreateLoading] = useState(false);
|
||||
const [roles, setRoles] = useState<{ roleId: number; name: string }[]>([]);
|
||||
const [roleMappingMode, setRoleMappingMode] = useState<
|
||||
"role" | "expression"
|
||||
>("role");
|
||||
const [roleMappingMode, setRoleMappingMode] =
|
||||
useState<RoleMappingMode>("fixedRoles");
|
||||
const [fixedRoleNames, setFixedRoleNames] = useState<string[]>([]);
|
||||
const [mappingBuilderClaimPath, setMappingBuilderClaimPath] =
|
||||
useState("groups");
|
||||
const [mappingBuilderRules, setMappingBuilderRules] = useState<
|
||||
MappingBuilderRule[]
|
||||
>([createMappingBuilderRule()]);
|
||||
const [rawRoleExpression, setRawRoleExpression] = useState("");
|
||||
const t = useTranslations();
|
||||
const { isPaidUser } = usePaidStatus();
|
||||
|
||||
@@ -228,7 +240,26 @@ export default function Page() {
|
||||
tokenUrl = tokenUrl?.replace("{{TENANT_ID}}", data.tenantId);
|
||||
}
|
||||
|
||||
const roleName = roles.find((r) => r.roleId === data.roleId)?.name;
|
||||
const roleMappingExpression = compileRoleMappingExpression({
|
||||
mode: roleMappingMode,
|
||||
fixedRoleNames,
|
||||
mappingBuilder: {
|
||||
claimPath: mappingBuilderClaimPath,
|
||||
rules: mappingBuilderRules
|
||||
},
|
||||
rawExpression: rawRoleExpression
|
||||
});
|
||||
|
||||
if (data.autoProvision && !roleMappingExpression) {
|
||||
toast({
|
||||
title: t("error"),
|
||||
description:
|
||||
"A role mapping is required when auto-provisioning is enabled.",
|
||||
variant: "destructive"
|
||||
});
|
||||
setCreateLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = {
|
||||
name: data.name,
|
||||
@@ -240,10 +271,7 @@ export default function Page() {
|
||||
emailPath: data.emailPath,
|
||||
namePath: data.namePath,
|
||||
autoProvision: data.autoProvision,
|
||||
roleMapping:
|
||||
roleMappingMode === "role"
|
||||
? `'${roleName}'`
|
||||
: data.roleMapping || "",
|
||||
roleMapping: roleMappingExpression,
|
||||
scopes: data.scopes,
|
||||
variant: data.type
|
||||
};
|
||||
@@ -363,43 +391,44 @@ export default function Page() {
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
<SettingsSectionForm>
|
||||
<PaidFeaturesAlert
|
||||
tiers={tierMatrix.autoProvisioning}
|
||||
/>
|
||||
<Form {...form}>
|
||||
<form
|
||||
className="space-y-4"
|
||||
id="create-idp-form"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
>
|
||||
<AutoProvisionConfigWidget
|
||||
control={form.control}
|
||||
autoProvision={
|
||||
form.watch(
|
||||
"autoProvision"
|
||||
) as boolean
|
||||
} // is this right?
|
||||
onAutoProvisionChange={(checked) => {
|
||||
form.setValue(
|
||||
"autoProvision",
|
||||
checked
|
||||
);
|
||||
}}
|
||||
roleMappingMode={roleMappingMode}
|
||||
onRoleMappingModeChange={(data) => {
|
||||
setRoleMappingMode(data);
|
||||
// Clear roleId and roleMapping when mode changes
|
||||
form.setValue("roleId", null);
|
||||
form.setValue("roleMapping", null);
|
||||
}}
|
||||
roles={roles}
|
||||
roleIdFieldName="roleId"
|
||||
roleMappingFieldName="roleMapping"
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
</SettingsSectionForm>
|
||||
<PaidFeaturesAlert
|
||||
tiers={tierMatrix.autoProvisioning}
|
||||
/>
|
||||
<Form {...form}>
|
||||
<form
|
||||
className="space-y-4"
|
||||
id="create-idp-form"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
>
|
||||
<AutoProvisionConfigWidget
|
||||
autoProvision={
|
||||
form.watch("autoProvision") as boolean
|
||||
} // is this right?
|
||||
onAutoProvisionChange={(checked) => {
|
||||
form.setValue("autoProvision", checked);
|
||||
}}
|
||||
roleMappingMode={roleMappingMode}
|
||||
onRoleMappingModeChange={(data) => {
|
||||
setRoleMappingMode(data);
|
||||
}}
|
||||
roles={roles}
|
||||
fixedRoleNames={fixedRoleNames}
|
||||
onFixedRoleNamesChange={setFixedRoleNames}
|
||||
mappingBuilderClaimPath={
|
||||
mappingBuilderClaimPath
|
||||
}
|
||||
onMappingBuilderClaimPathChange={
|
||||
setMappingBuilderClaimPath
|
||||
}
|
||||
mappingBuilderRules={mappingBuilderRules}
|
||||
onMappingBuilderRulesChange={
|
||||
setMappingBuilderRules
|
||||
}
|
||||
rawExpression={rawRoleExpression}
|
||||
onRawExpressionChange={setRawRoleExpression}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
</SettingsSectionBody>
|
||||
</SettingsSection>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user