basic idp mapping builder

This commit is contained in:
miloschwartz
2026-03-26 21:46:01 -07:00
parent 13eadeaa8f
commit ad7d68d2b4
5 changed files with 755 additions and 240 deletions

View File

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

View File

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