diff --git a/messages/en-US.json b/messages/en-US.json index 361771d87..7d00c8105 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -509,6 +509,7 @@ "userSaved": "User saved", "userSavedDescription": "The user has been updated.", "autoProvisioned": "Auto Provisioned", + "autoProvisionSettings": "Auto Provision Settings", "autoProvisionedDescription": "Allow this user to be automatically managed by identity provider", "accessControlsDescription": "Manage what this user can access and do in the organization", "accessControlsSubmit": "Save Access Controls", @@ -1042,7 +1043,6 @@ "pageNotFoundDescription": "Oops! The page you're looking for doesn't exist.", "overview": "Overview", "home": "Home", - "accessControl": "Access Control", "settings": "Settings", "usersAll": "All Users", "license": "License", @@ -1942,6 +1942,24 @@ "invalidValue": "Invalid value", "idpTypeLabel": "Identity Provider Type", "roleMappingExpressionPlaceholder": "e.g., contains(groups, 'admin') && 'Admin' || 'Member'", + "roleMappingModeFixedRoles": "Fixed roles", + "roleMappingModeMappingBuilder": "Mapping builder", + "roleMappingModeRawExpression": "Raw expression", + "roleMappingFixedRolesPlaceholderSelect": "Select one or more roles", + "roleMappingFixedRolesPlaceholderFreeform": "Type role names (exact match per organization)", + "roleMappingFixedRolesDescriptionSameForAll": "Assign the same role set to every auto-provisioned user.", + "roleMappingFixedRolesDescriptionDefaultPolicy": "For default policies, type role names that exist in each organization where users are provisioned. Names must match exactly.", + "roleMappingClaimPath": "Claim path", + "roleMappingClaimPathPlaceholder": "groups", + "roleMappingClaimPathDescription": "Path in the token payload that contains source values (for example, groups).", + "roleMappingMatchValue": "Match value", + "roleMappingAssignRoles": "Assign roles", + "roleMappingAddMappingRule": "Add mapping rule", + "roleMappingRawExpressionResultDescription": "Expression must evaluate to a string or string array.", + "roleMappingMatchValuePlaceholder": "Match value (for example: admin)", + "roleMappingAssignRolesPlaceholderFreeform": "Type role names (exact per org)", + "roleMappingBuilderFreeformRowHint": "Role names must match a role in each target organization.", + "roleMappingRemoveRule": "Remove", "idpGoogleConfiguration": "Google Configuration", "idpGoogleConfigurationDescription": "Configure the Google OAuth2 credentials", "idpGoogleClientIdDescription": "Google OAuth2 Client ID", @@ -2514,9 +2532,9 @@ "remoteExitNodeRegenerateCredentialsConfirmation": "Are you sure you want to regenerate the credentials for this remote exit node?", "remoteExitNodeRegenerateCredentialsWarning": "This will regenerate the credentials. The remote exit node will stay connected until you manually restart it and use the new credentials.", "agent": "Agent", - "personalUseOnly": "Personal Use Only", - "loginPageLicenseWatermark": "This instance is licensed for personal use only.", - "instanceIsUnlicensed": "This instance is unlicensed.", + "personalUseOnly": "Personal Use Only", + "loginPageLicenseWatermark": "This instance is licensed for personal use only.", + "instanceIsUnlicensed": "This instance is unlicensed.", "portRestrictions": "Port Restrictions", "allPorts": "All", "custom": "Custom", @@ -2570,7 +2588,7 @@ "automaticModeDescription": " Show maintenance page only when all backend targets are down or unhealthy. Your resource continues working normally as long as at least one target is healthy.", "forced": "Forced", "forcedModeDescription": "Always show the maintenance page regardless of backend health. Use this for planned maintenance when you want to prevent all access.", - "warning:" : "Warning:", + "warning:": "Warning:", "forcedeModeWarning": "All traffic will be directed to the maintenance page. Your backend resources will not receive any requests.", "pageTitle": "Page Title", "pageTitleDescription": "The main heading displayed on the maintenance page", @@ -2687,5 +2705,6 @@ "approvalsEmptyStateStep2Description": "Edit a role and enable the 'Require Device Approvals' option. Users with this role will need admin approval for new devices.", "approvalsEmptyStatePreviewDescription": "Preview: When enabled, pending device requests will appear here for review", "approvalsEmptyStateButtonText": "Manage Roles", - "domainErrorTitle": "We are having trouble verifying your domain" + "domainErrorTitle": "We are having trouble verifying your domain", + "idpAdminAutoProvisionPoliciesTabHint": "Configure role mapping and organization policies on the Auto Provision Settings tab." } diff --git a/src/app/admin/idp/[idpId]/general/page.tsx b/src/app/admin/idp/[idpId]/general/page.tsx index d431efa2d..a5ed14a6e 100644 --- a/src/app/admin/idp/[idpId]/general/page.tsx +++ b/src/app/admin/idp/[idpId]/general/page.tsx @@ -15,7 +15,8 @@ import { import { Input } from "@app/components/ui/input"; import { useForm } from "react-hook-form"; import { toast } from "@app/hooks/useToast"; -import { useRouter, useParams, redirect } from "next/navigation"; +import { useRouter, useParams } from "next/navigation"; +import Link from "next/link"; import { SettingsContainer, SettingsSection, @@ -189,15 +190,6 @@ export default function GeneralPage() { - - - - {t("redirectUrlAbout")} - - - {t("redirectUrlAboutDescription")} - -
- - {t("idpAutoProvisionUsersDescription")} - +
+ + {t( + "idpAutoProvisionUsersDescription" + )} + + {form.watch("autoProvision") && ( + + {t.rich( + "idpAdminAutoProvisionPoliciesTabHint", + { + policiesTabLink: ( + chunks + ) => ( + + {chunks} + + ) + } + )} + + )} +
@@ -375,29 +390,6 @@ export default function GeneralPage() { className="space-y-4" id="general-settings-form" > - - - - {t("idpJmespathAbout")} - - - {t( - "idpJmespathAboutDescription" - )} - - {t( - "idpJmespathAboutDescriptionLink" - )}{" "} - - - - - void, + setFixed: (v: string[]) => void, + setClaim: (v: string) => void, + setRules: (v: MappingBuilderRule[]) => void, + setRaw: (v: string) => void, + stored: string | null | undefined +) { + const d = detectRoleMappingConfig(stored); + setMode(d.mode); + setFixed(d.fixedRoleNames); + setClaim(d.mappingBuilder.claimPath); + setRules(d.mappingBuilder.rules); + setRaw(d.rawExpression); +} + export default function PoliciesPage() { const { env } = useEnvContext(); const api = createApiClient({ env }); - const router = useRouter(); const { idpId } = useParams(); const t = useTranslations(); @@ -88,14 +111,39 @@ export default function PoliciesPage() { const [showAddDialog, setShowAddDialog] = useState(false); const [editingPolicy, setEditingPolicy] = useState(null); + const [defaultRoleMappingMode, setDefaultRoleMappingMode] = + useState("fixedRoles"); + const [defaultFixedRoleNames, setDefaultFixedRoleNames] = useState< + string[] + >([]); + const [defaultMappingBuilderClaimPath, setDefaultMappingBuilderClaimPath] = + useState("groups"); + const [defaultMappingBuilderRules, setDefaultMappingBuilderRules] = + useState([createMappingBuilderRule()]); + const [defaultRawRoleExpression, setDefaultRawRoleExpression] = + useState(""); + + const [policyRoleMappingMode, setPolicyRoleMappingMode] = + useState("fixedRoles"); + const [policyFixedRoleNames, setPolicyFixedRoleNames] = useState( + [] + ); + const [policyMappingBuilderClaimPath, setPolicyMappingBuilderClaimPath] = + useState("groups"); + const [policyMappingBuilderRules, setPolicyMappingBuilderRules] = useState< + MappingBuilderRule[] + >([createMappingBuilderRule()]); + const [policyRawRoleExpression, setPolicyRawRoleExpression] = useState(""); + const [policyOrgRoles, setPolicyOrgRoles] = useState< + { roleId: number; name: string }[] + >([]); + const policyFormSchema = z.object({ orgId: z.string().min(1, { message: t("orgRequired") }), - roleMapping: z.string().optional(), orgMapping: z.string().optional() }); const defaultMappingsSchema = z.object({ - defaultRoleMapping: z.string().optional(), defaultOrgMapping: z.string().optional() }); @@ -106,15 +154,15 @@ export default function PoliciesPage() { resolver: zodResolver(policyFormSchema), defaultValues: { orgId: "", - roleMapping: "", orgMapping: "" } }); + const policyFormOrgId = form.watch("orgId"); + const defaultMappingsForm = useForm({ resolver: zodResolver(defaultMappingsSchema), defaultValues: { - defaultRoleMapping: "", defaultOrgMapping: "" } }); @@ -127,9 +175,16 @@ export default function PoliciesPage() { if (res.status === 200) { const data = res.data.data; defaultMappingsForm.reset({ - defaultRoleMapping: data.idp.defaultRoleMapping || "", defaultOrgMapping: data.idp.defaultOrgMapping || "" }); + resetRoleMappingStateFromDetected( + setDefaultRoleMappingMode, + setDefaultFixedRoleNames, + setDefaultMappingBuilderClaimPath, + setDefaultMappingBuilderRules, + setDefaultRawRoleExpression, + data.idp.defaultRoleMapping + ); } } catch (e) { toast({ @@ -184,11 +239,67 @@ export default function PoliciesPage() { load(); }, [idpId]); + useEffect(() => { + if (!showAddDialog) { + return; + } + + const orgId = editingPolicy?.orgId || policyFormOrgId; + if (!orgId) { + setPolicyOrgRoles([]); + return; + } + + let cancelled = false; + (async () => { + const res = await api + .get>(`/org/${orgId}/roles`) + .catch((e) => { + console.error(e); + toast({ + variant: "destructive", + title: t("accessRoleErrorFetch"), + description: formatAxiosError( + e, + t("accessRoleErrorFetchDescription") + ) + }); + return null; + }); + if (!cancelled && res?.status === 200) { + setPolicyOrgRoles(res.data.data.roles); + } + })(); + + return () => { + cancelled = true; + }; + }, [showAddDialog, editingPolicy?.orgId, policyFormOrgId, api, t]); + + function resetPolicyDialogRoleMappingState() { + const d = defaultRoleMappingConfig(); + setPolicyRoleMappingMode(d.mode); + setPolicyFixedRoleNames(d.fixedRoleNames); + setPolicyMappingBuilderClaimPath(d.mappingBuilder.claimPath); + setPolicyMappingBuilderRules(d.mappingBuilder.rules); + setPolicyRawRoleExpression(d.rawExpression); + } + const onAddPolicy = async (data: PolicyFormValues) => { + const roleMappingExpression = compileRoleMappingExpression({ + mode: policyRoleMappingMode, + fixedRoleNames: policyFixedRoleNames, + mappingBuilder: { + claimPath: policyMappingBuilderClaimPath, + rules: policyMappingBuilderRules + }, + rawExpression: policyRawRoleExpression + }); + setAddPolicyLoading(true); try { const res = await api.put(`/idp/${idpId}/org/${data.orgId}`, { - roleMapping: data.roleMapping, + roleMapping: roleMappingExpression, orgMapping: data.orgMapping }); if (res.status === 201) { @@ -197,7 +308,7 @@ export default function PoliciesPage() { name: organizations.find((org) => org.orgId === data.orgId) ?.name || "", - roleMapping: data.roleMapping, + roleMapping: roleMappingExpression, orgMapping: data.orgMapping }; setPolicies([...policies, newPolicy]); @@ -207,6 +318,7 @@ export default function PoliciesPage() { }); setShowAddDialog(false); form.reset(); + resetPolicyDialogRoleMappingState(); } } catch (e) { toast({ @@ -222,12 +334,22 @@ export default function PoliciesPage() { const onEditPolicy = async (data: PolicyFormValues) => { if (!editingPolicy) return; + const roleMappingExpression = compileRoleMappingExpression({ + mode: policyRoleMappingMode, + fixedRoleNames: policyFixedRoleNames, + mappingBuilder: { + claimPath: policyMappingBuilderClaimPath, + rules: policyMappingBuilderRules + }, + rawExpression: policyRawRoleExpression + }); + setEditPolicyLoading(true); try { const res = await api.post( `/idp/${idpId}/org/${editingPolicy.orgId}`, { - roleMapping: data.roleMapping, + roleMapping: roleMappingExpression, orgMapping: data.orgMapping } ); @@ -237,7 +359,7 @@ export default function PoliciesPage() { policy.orgId === editingPolicy.orgId ? { ...policy, - roleMapping: data.roleMapping, + roleMapping: roleMappingExpression, orgMapping: data.orgMapping } : policy @@ -250,6 +372,7 @@ export default function PoliciesPage() { setShowAddDialog(false); setEditingPolicy(null); form.reset(); + resetPolicyDialogRoleMappingState(); } } catch (e) { toast({ @@ -287,10 +410,20 @@ export default function PoliciesPage() { }; const onUpdateDefaultMappings = async (data: DefaultMappingsValues) => { + const defaultRoleMappingExpression = compileRoleMappingExpression({ + mode: defaultRoleMappingMode, + fixedRoleNames: defaultFixedRoleNames, + mappingBuilder: { + claimPath: defaultMappingBuilderClaimPath, + rules: defaultMappingBuilderRules + }, + rawExpression: defaultRawRoleExpression + }); + setUpdateDefaultMappingsLoading(true); try { const res = await api.post(`/idp/${idpId}/oidc`, { - defaultRoleMapping: data.defaultRoleMapping, + defaultRoleMapping: defaultRoleMappingExpression, defaultOrgMapping: data.defaultOrgMapping }); if (res.status === 200) { @@ -317,25 +450,36 @@ export default function PoliciesPage() { return ( <> - - - - {t("orgPoliciesAbout")} - - - {/*TODO(vlalx): Validate replacing */} - {t("orgPoliciesAboutDescription")}{" "} - - {t("orgPoliciesAboutDescriptionLink")} - - - - + { + loadOrganizations(); + form.reset({ + orgId: "", + orgMapping: "" + }); + setEditingPolicy(null); + resetPolicyDialogRoleMappingState(); + setShowAddDialog(true); + }} + onEdit={(policy) => { + setEditingPolicy(policy); + form.reset({ + orgId: policy.orgId, + orgMapping: policy.orgMapping || "" + }); + resetRoleMappingStateFromDetected( + setPolicyRoleMappingMode, + setPolicyFixedRoleNames, + setPolicyMappingBuilderClaimPath, + setPolicyMappingBuilderRules, + setPolicyRawRoleExpression, + policy.roleMapping + ); + setShowAddDialog(true); + }} + /> @@ -353,51 +497,58 @@ export default function PoliciesPage() { onUpdateDefaultMappings )} id="policy-default-mappings-form" - className="space-y-4" + className="space-y-6" > -
- ( - - - {t("defaultMappingsRole")} - - - - - - {t( - "defaultMappingsRoleDescription" - )} - - - - )} - /> + - ( - - - {t("defaultMappingsOrg")} - - - - - - {t( - "defaultMappingsOrgDescription" - )} - - - - )} - /> -
+ ( + + + {t("defaultMappingsOrg")} + + + + + + {t( + "defaultMappingsOrgDescription" + )} + + + + )} + /> @@ -411,41 +562,20 @@ export default function PoliciesPage() {
- - { - loadOrganizations(); - form.reset({ - orgId: "", - roleMapping: "", - orgMapping: "" - }); - setEditingPolicy(null); - setShowAddDialog(true); - }} - onEdit={(policy) => { - setEditingPolicy(policy); - form.reset({ - orgId: policy.orgId, - roleMapping: policy.roleMapping || "", - orgMapping: policy.orgMapping || "" - }); - setShowAddDialog(true); - }} - />
{ setShowAddDialog(val); - setEditingPolicy(null); - form.reset(); + if (!val) { + setEditingPolicy(null); + form.reset(); + resetPolicyDialogRoleMappingState(); + } }} > - + {editingPolicy @@ -456,7 +586,7 @@ export default function PoliciesPage() { {t("orgPolicyConfig")} - +
- ( - - - {t("roleMappingPathOptional")} - - - - - - {t( - "defaultMappingsRoleDescription" - )} - - - - )} + - - - - - {t("idpOidcConfigureAlert")} - - - {t("idpOidcConfigureAlertDescription")} - - @@ -369,29 +359,6 @@ export default function Page() { id="create-idp-form" onSubmit={form.handleSubmit(onSubmit)} > - - - - {t("idpJmespathAbout")} - - - {t( - "idpJmespathAboutDescription" - )}{" "} - - {t( - "idpJmespathAboutDescriptionLink" - )}{" "} - - - - - (null); - - const roleOptions = useMemo( - () => - roles.map((role) => ({ - id: role.name, - text: role.name - })), - [roles] - ); return (
@@ -80,261 +60,30 @@ export default function AutoProvisionConfigWidget({ onCheckedChange={onAutoProvisionChange} disabled={!isPaidUser(tierMatrix.autoProvisioning)} /> - + {t("idpAutoProvisionUsersDescription")} - +
{autoProvision && ( -
-
- - {t("roleMapping")} - - - {t("roleMappingDescription")} - - - -
- - -
-
- - -
-
- - -
-
-
- - {roleMappingMode === "fixedRoles" && ( -
- ({ - id: name, - text: name - }))} - setTags={(nextTags) => { - const next = - typeof nextTags === "function" - ? nextTags( - fixedRoleNames.map((name) => ({ - id: name, - text: name - })) - ) - : nextTags; - - onFixedRoleNamesChange( - [...new Set(next.map((tag) => tag.text))] - ); - }} - activeTagIndex={activeFixedRoleTagIndex} - setActiveTagIndex={setActiveFixedRoleTagIndex} - placeholder="Select one or more roles" - enableAutocomplete={true} - autocompleteOptions={roleOptions} - restrictTagsToAutocompleteOptions={true} - allowDuplicates={false} - sortTags={true} - size="sm" - /> - - Assign the same role set to every auto-provisioned - user. - -
- )} - - {roleMappingMode === "mappingBuilder" && ( -
-
- Claim path - - onMappingBuilderClaimPathChange( - e.target.value - ) - } - placeholder="groups" - /> - - Path in the token payload that contains source - values (for example, groups). - -
- -
-
- Match value - Assign roles - -
- - {mappingBuilderRules.map((rule, index) => ( - { - const nextRules = - mappingBuilderRules.map( - (row, i) => - i === index - ? nextRule - : row - ); - onMappingBuilderRulesChange( - nextRules - ); - }} - onRemove={() => { - const nextRules = - mappingBuilderRules.filter( - (_, i) => i !== index - ); - onMappingBuilderRulesChange( - nextRules.length - ? nextRules - : [createMappingBuilderRule()] - ); - }} - /> - ))} -
- - -
- )} - - {roleMappingMode === "rawExpression" && ( -
- - onRawExpressionChange(e.target.value) - } - placeholder={t("roleMappingExpressionPlaceholder")} - /> - - Expression must evaluate to a string or string - array. - -
- )} -
+ )} ); } - -function BuilderRuleRow({ - rule, - roleOptions, - onChange, - onRemove -}: { - rule: MappingBuilderRule; - roleOptions: Tag[]; - onChange: (rule: MappingBuilderRule) => void; - onRemove: () => void; -}) { - const [activeTagIndex, setActiveTagIndex] = useState(null); - - return ( -
-
- Match value - - onChange({ - ...rule, - matchValue: e.target.value - }) - } - placeholder="Match value (for example: admin)" - /> -
-
- Assign roles - ({ id: name, text: name }))} - setTags={(nextTags) => { - const next = - typeof nextTags === "function" - ? nextTags( - rule.roleNames.map((name) => ({ - id: name, - text: name - })) - ) - : nextTags; - onChange({ - ...rule, - roleNames: [...new Set(next.map((tag) => tag.text))] - }); - }} - activeTagIndex={activeTagIndex} - setActiveTagIndex={setActiveTagIndex} - placeholder="Assign roles" - enableAutocomplete={true} - autocompleteOptions={roleOptions} - restrictTagsToAutocompleteOptions={true} - allowDuplicates={false} - sortTags={true} - size="sm" - /> -
-
- -
-
- ); -} diff --git a/src/components/RoleMappingConfigFields.tsx b/src/components/RoleMappingConfigFields.tsx new file mode 100644 index 000000000..4fe1a037b --- /dev/null +++ b/src/components/RoleMappingConfigFields.tsx @@ -0,0 +1,366 @@ +"use client"; + +import { FormLabel, FormDescription } from "@app/components/ui/form"; +import { RadioGroup, RadioGroupItem } from "@app/components/ui/radio-group"; +import { Button } from "@app/components/ui/button"; +import { Input } from "@app/components/ui/input"; +import { useTranslations } from "next-intl"; +import { useMemo, useState } from "react"; +import { Tag, TagInput } from "@app/components/tags/tag-input"; +import { + createMappingBuilderRule, + MappingBuilderRule, + RoleMappingMode +} from "@app/lib/idpRoleMapping"; + +export type RoleMappingRoleOption = { + roleId: number; + name: string; +}; + +export type RoleMappingConfigFieldsProps = { + roleMappingMode: RoleMappingMode; + onRoleMappingModeChange: (mode: RoleMappingMode) => void; + roles: RoleMappingRoleOption[]; + fixedRoleNames: string[]; + onFixedRoleNamesChange: (roleNames: string[]) => void; + mappingBuilderClaimPath: string; + onMappingBuilderClaimPathChange: (claimPath: string) => void; + mappingBuilderRules: MappingBuilderRule[]; + onMappingBuilderRulesChange: (rules: MappingBuilderRule[]) => void; + rawExpression: string; + onRawExpressionChange: (expression: string) => void; + /** Unique prefix for radio `id`/`htmlFor` when multiple instances exist on one page. */ + fieldIdPrefix?: string; + /** When true, show extra hint for global default policies (no org role list). */ + showFreeformRoleNamesHint?: boolean; +}; + +export default function RoleMappingConfigFields({ + roleMappingMode, + onRoleMappingModeChange, + roles, + fixedRoleNames, + onFixedRoleNamesChange, + mappingBuilderClaimPath, + onMappingBuilderClaimPathChange, + mappingBuilderRules, + onMappingBuilderRulesChange, + rawExpression, + onRawExpressionChange, + fieldIdPrefix = "role-mapping", + showFreeformRoleNamesHint = false +}: RoleMappingConfigFieldsProps) { + const t = useTranslations(); + const [activeFixedRoleTagIndex, setActiveFixedRoleTagIndex] = useState< + number | null + >(null); + + const restrictToOrgRoles = roles.length > 0; + + const roleOptions = useMemo( + () => + roles.map((role) => ({ + id: role.name, + text: role.name + })), + [roles] + ); + + const fixedRadioId = `${fieldIdPrefix}-fixed-roles-mode`; + const builderRadioId = `${fieldIdPrefix}-mapping-builder-mode`; + const rawRadioId = `${fieldIdPrefix}-raw-expression-mode`; + + /** Same template on header + rows so 1fr/1.75fr columns line up (auto third col differs per row otherwise). */ + const mappingRulesGridClass = + "md:grid md:grid-cols-[minmax(0,1fr)_minmax(0,1.75fr)_6rem] md:gap-x-3"; + + return ( +
+
+ {t("roleMapping")} + + {t("roleMappingDescription")} + + + +
+ + +
+
+ + +
+
+ + +
+
+
+ + {roleMappingMode === "fixedRoles" && ( +
+ ({ + id: name, + text: name + }))} + setTags={(nextTags) => { + const next = + typeof nextTags === "function" + ? nextTags( + fixedRoleNames.map((name) => ({ + id: name, + text: name + })) + ) + : nextTags; + + onFixedRoleNamesChange([ + ...new Set(next.map((tag) => tag.text)) + ]); + }} + activeTagIndex={activeFixedRoleTagIndex} + setActiveTagIndex={setActiveFixedRoleTagIndex} + placeholder={ + restrictToOrgRoles + ? t("roleMappingFixedRolesPlaceholderSelect") + : t("roleMappingFixedRolesPlaceholderFreeform") + } + enableAutocomplete={restrictToOrgRoles} + autocompleteOptions={roleOptions} + restrictTagsToAutocompleteOptions={restrictToOrgRoles} + allowDuplicates={false} + sortTags={true} + size="sm" + /> + + {showFreeformRoleNamesHint + ? t("roleMappingFixedRolesDescriptionDefaultPolicy") + : t("roleMappingFixedRolesDescriptionSameForAll")} + +
+ )} + + {roleMappingMode === "mappingBuilder" && ( +
+
+ {t("roleMappingClaimPath")} + + onMappingBuilderClaimPathChange(e.target.value) + } + placeholder={t("roleMappingClaimPathPlaceholder")} + /> + + {t("roleMappingClaimPathDescription")} + +
+ +
+
+ + {t("roleMappingMatchValue")} + + + {t("roleMappingAssignRoles")} + + +
+ + {mappingBuilderRules.map((rule, index) => ( + { + const nextRules = mappingBuilderRules.map( + (row, i) => + i === index ? nextRule : row + ); + onMappingBuilderRulesChange(nextRules); + }} + onRemove={() => { + const nextRules = + mappingBuilderRules.filter( + (_, i) => i !== index + ); + onMappingBuilderRulesChange( + nextRules.length + ? nextRules + : [createMappingBuilderRule()] + ); + }} + /> + ))} +
+ + +
+ )} + + {roleMappingMode === "rawExpression" && ( +
+ onRawExpressionChange(e.target.value)} + placeholder={t("roleMappingExpressionPlaceholder")} + /> + + {t("roleMappingRawExpressionResultDescription")} + +
+ )} +
+ ); +} + +function BuilderRuleRow({ + rule, + roleOptions, + restrictToOrgRoles, + showFreeformRoleNamesHint, + fieldIdPrefix, + mappingRulesGridClass, + onChange, + onRemove +}: { + rule: MappingBuilderRule; + roleOptions: Tag[]; + restrictToOrgRoles: boolean; + showFreeformRoleNamesHint: boolean; + fieldIdPrefix: string; + mappingRulesGridClass: string; + onChange: (rule: MappingBuilderRule) => void; + onRemove: () => void; +}) { + const t = useTranslations(); + const [activeTagIndex, setActiveTagIndex] = useState(null); + + return ( +
+
+ + {t("roleMappingMatchValue")} + + + onChange({ + ...rule, + matchValue: e.target.value + }) + } + placeholder={t("roleMappingMatchValuePlaceholder")} + /> +
+
+ + {t("roleMappingAssignRoles")} + +
+ ({ + id: name, + text: name + }))} + setTags={(nextTags) => { + const next = + typeof nextTags === "function" + ? nextTags( + rule.roleNames.map((name) => ({ + id: name, + text: name + })) + ) + : nextTags; + onChange({ + ...rule, + roleNames: [ + ...new Set(next.map((tag) => tag.text)) + ] + }); + }} + activeTagIndex={activeTagIndex} + setActiveTagIndex={setActiveTagIndex} + placeholder={ + restrictToOrgRoles + ? t("roleMappingAssignRoles") + : t("roleMappingAssignRolesPlaceholderFreeform") + } + enableAutocomplete={restrictToOrgRoles} + autocompleteOptions={roleOptions} + restrictTagsToAutocompleteOptions={restrictToOrgRoles} + allowDuplicates={false} + sortTags={true} + size="sm" + styleClasses={{ + inlineTagsContainer: "min-w-0 max-w-full" + }} + /> +
+ {showFreeformRoleNamesHint && ( +

+ {t("roleMappingBuilderFreeformRowHint")} +

+ )} +
+
+ +
+
+ ); +}