mirror of
https://github.com/fosrl/pangolin.git
synced 2026-03-30 06:26:39 +00:00
support policy buildiner in global idp
This commit is contained in:
@@ -509,6 +509,7 @@
|
|||||||
"userSaved": "User saved",
|
"userSaved": "User saved",
|
||||||
"userSavedDescription": "The user has been updated.",
|
"userSavedDescription": "The user has been updated.",
|
||||||
"autoProvisioned": "Auto Provisioned",
|
"autoProvisioned": "Auto Provisioned",
|
||||||
|
"autoProvisionSettings": "Auto Provision Settings",
|
||||||
"autoProvisionedDescription": "Allow this user to be automatically managed by identity provider",
|
"autoProvisionedDescription": "Allow this user to be automatically managed by identity provider",
|
||||||
"accessControlsDescription": "Manage what this user can access and do in the organization",
|
"accessControlsDescription": "Manage what this user can access and do in the organization",
|
||||||
"accessControlsSubmit": "Save Access Controls",
|
"accessControlsSubmit": "Save Access Controls",
|
||||||
@@ -1042,7 +1043,6 @@
|
|||||||
"pageNotFoundDescription": "Oops! The page you're looking for doesn't exist.",
|
"pageNotFoundDescription": "Oops! The page you're looking for doesn't exist.",
|
||||||
"overview": "Overview",
|
"overview": "Overview",
|
||||||
"home": "Home",
|
"home": "Home",
|
||||||
"accessControl": "Access Control",
|
|
||||||
"settings": "Settings",
|
"settings": "Settings",
|
||||||
"usersAll": "All Users",
|
"usersAll": "All Users",
|
||||||
"license": "License",
|
"license": "License",
|
||||||
@@ -1942,6 +1942,24 @@
|
|||||||
"invalidValue": "Invalid value",
|
"invalidValue": "Invalid value",
|
||||||
"idpTypeLabel": "Identity Provider Type",
|
"idpTypeLabel": "Identity Provider Type",
|
||||||
"roleMappingExpressionPlaceholder": "e.g., contains(groups, 'admin') && 'Admin' || 'Member'",
|
"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",
|
"idpGoogleConfiguration": "Google Configuration",
|
||||||
"idpGoogleConfigurationDescription": "Configure the Google OAuth2 credentials",
|
"idpGoogleConfigurationDescription": "Configure the Google OAuth2 credentials",
|
||||||
"idpGoogleClientIdDescription": "Google OAuth2 Client ID",
|
"idpGoogleClientIdDescription": "Google OAuth2 Client ID",
|
||||||
@@ -2514,9 +2532,9 @@
|
|||||||
"remoteExitNodeRegenerateCredentialsConfirmation": "Are you sure you want to regenerate the credentials for this remote exit node?",
|
"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.",
|
"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",
|
"agent": "Agent",
|
||||||
"personalUseOnly": "Personal Use Only",
|
"personalUseOnly": "Personal Use Only",
|
||||||
"loginPageLicenseWatermark": "This instance is licensed for personal use only.",
|
"loginPageLicenseWatermark": "This instance is licensed for personal use only.",
|
||||||
"instanceIsUnlicensed": "This instance is unlicensed.",
|
"instanceIsUnlicensed": "This instance is unlicensed.",
|
||||||
"portRestrictions": "Port Restrictions",
|
"portRestrictions": "Port Restrictions",
|
||||||
"allPorts": "All",
|
"allPorts": "All",
|
||||||
"custom": "Custom",
|
"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.",
|
"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",
|
"forced": "Forced",
|
||||||
"forcedModeDescription": "Always show the maintenance page regardless of backend health. Use this for planned maintenance when you want to prevent all access.",
|
"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.",
|
"forcedeModeWarning": "All traffic will be directed to the maintenance page. Your backend resources will not receive any requests.",
|
||||||
"pageTitle": "Page Title",
|
"pageTitle": "Page Title",
|
||||||
"pageTitleDescription": "The main heading displayed on the maintenance page",
|
"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.",
|
"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",
|
"approvalsEmptyStatePreviewDescription": "Preview: When enabled, pending device requests will appear here for review",
|
||||||
"approvalsEmptyStateButtonText": "Manage Roles",
|
"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 <policiesTabLink>Auto Provision Settings</policiesTabLink> tab."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ import {
|
|||||||
import { Input } from "@app/components/ui/input";
|
import { Input } from "@app/components/ui/input";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "@app/hooks/useToast";
|
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 {
|
import {
|
||||||
SettingsContainer,
|
SettingsContainer,
|
||||||
SettingsSection,
|
SettingsSection,
|
||||||
@@ -189,15 +190,6 @@ export default function GeneralPage() {
|
|||||||
</InfoSection>
|
</InfoSection>
|
||||||
</InfoSections>
|
</InfoSections>
|
||||||
|
|
||||||
<Alert variant="neutral" className="">
|
|
||||||
<InfoIcon className="h-4 w-4" />
|
|
||||||
<AlertTitle className="font-semibold">
|
|
||||||
{t("redirectUrlAbout")}
|
|
||||||
</AlertTitle>
|
|
||||||
<AlertDescription>
|
|
||||||
{t("redirectUrlAboutDescription")}
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
<SettingsSectionForm>
|
<SettingsSectionForm>
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
@@ -239,9 +231,32 @@ export default function GeneralPage() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-sm text-muted-foreground">
|
<div className="flex flex-col gap-2">
|
||||||
{t("idpAutoProvisionUsersDescription")}
|
<span className="text-sm text-muted-foreground">
|
||||||
</span>
|
{t(
|
||||||
|
"idpAutoProvisionUsersDescription"
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
{form.watch("autoProvision") && (
|
||||||
|
<FormDescription>
|
||||||
|
{t.rich(
|
||||||
|
"idpAdminAutoProvisionPoliciesTabHint",
|
||||||
|
{
|
||||||
|
policiesTabLink: (
|
||||||
|
chunks
|
||||||
|
) => (
|
||||||
|
<Link
|
||||||
|
href={`/admin/idp/${idpId}/policies`}
|
||||||
|
className="text-primary hover:underline inline-flex items-center gap-1"
|
||||||
|
>
|
||||||
|
{chunks}
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</FormDescription>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</SettingsSectionForm>
|
</SettingsSectionForm>
|
||||||
@@ -375,29 +390,6 @@ export default function GeneralPage() {
|
|||||||
className="space-y-4"
|
className="space-y-4"
|
||||||
id="general-settings-form"
|
id="general-settings-form"
|
||||||
>
|
>
|
||||||
<Alert variant="neutral">
|
|
||||||
<InfoIcon className="h-4 w-4" />
|
|
||||||
<AlertTitle className="font-semibold">
|
|
||||||
{t("idpJmespathAbout")}
|
|
||||||
</AlertTitle>
|
|
||||||
<AlertDescription>
|
|
||||||
{t(
|
|
||||||
"idpJmespathAboutDescription"
|
|
||||||
)}
|
|
||||||
<a
|
|
||||||
href="https://jmespath.org"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="text-primary hover:underline inline-flex items-center"
|
|
||||||
>
|
|
||||||
{t(
|
|
||||||
"idpJmespathAboutDescriptionLink"
|
|
||||||
)}{" "}
|
|
||||||
<ExternalLink className="ml-1 h-4 w-4" />
|
|
||||||
</a>
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="identifierPath"
|
name="identifierPath"
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
|
|||||||
href: `/admin/idp/${params.idpId}/general`
|
href: `/admin/idp/${params.idpId}/general`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("orgPolicies"),
|
title: t("autoProvisionSettings"),
|
||||||
href: `/admin/idp/${params.idpId}/policies`
|
href: `/admin/idp/${params.idpId}/policies`
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useParams, useRouter } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
import { toast } from "@app/hooks/useToast";
|
import { toast } from "@app/hooks/useToast";
|
||||||
@@ -34,6 +34,7 @@ import { InfoIcon, ExternalLink, CheckIcon } from "lucide-react";
|
|||||||
import PolicyTable, { PolicyRow } from "../../../../../components/PolicyTable";
|
import PolicyTable, { PolicyRow } from "../../../../../components/PolicyTable";
|
||||||
import { AxiosResponse } from "axios";
|
import { AxiosResponse } from "axios";
|
||||||
import { ListOrgsResponse } from "@server/routers/org";
|
import { ListOrgsResponse } from "@server/routers/org";
|
||||||
|
import { ListRolesResponse } from "@server/routers/role";
|
||||||
import {
|
import {
|
||||||
Popover,
|
Popover,
|
||||||
PopoverContent,
|
PopoverContent,
|
||||||
@@ -50,8 +51,6 @@ import {
|
|||||||
} from "@app/components/ui/command";
|
} from "@app/components/ui/command";
|
||||||
import { CaretSortIcon } from "@radix-ui/react-icons";
|
import { CaretSortIcon } from "@radix-ui/react-icons";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Textarea } from "@app/components/ui/textarea";
|
|
||||||
import { InfoPopup } from "@app/components/ui/info-popup";
|
|
||||||
import { GetIdpResponse } from "@server/routers/idp";
|
import { GetIdpResponse } from "@server/routers/idp";
|
||||||
import {
|
import {
|
||||||
SettingsContainer,
|
SettingsContainer,
|
||||||
@@ -64,16 +63,40 @@ import {
|
|||||||
SettingsSectionForm
|
SettingsSectionForm
|
||||||
} from "@app/components/Settings";
|
} from "@app/components/Settings";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
import RoleMappingConfigFields from "@app/components/RoleMappingConfigFields";
|
||||||
|
import {
|
||||||
|
compileRoleMappingExpression,
|
||||||
|
createMappingBuilderRule,
|
||||||
|
defaultRoleMappingConfig,
|
||||||
|
detectRoleMappingConfig,
|
||||||
|
MappingBuilderRule,
|
||||||
|
RoleMappingMode
|
||||||
|
} from "@app/lib/idpRoleMapping";
|
||||||
|
|
||||||
type Organization = {
|
type Organization = {
|
||||||
orgId: string;
|
orgId: string;
|
||||||
name: string;
|
name: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function resetRoleMappingStateFromDetected(
|
||||||
|
setMode: (m: RoleMappingMode) => 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() {
|
export default function PoliciesPage() {
|
||||||
const { env } = useEnvContext();
|
const { env } = useEnvContext();
|
||||||
const api = createApiClient({ env });
|
const api = createApiClient({ env });
|
||||||
const router = useRouter();
|
|
||||||
const { idpId } = useParams();
|
const { idpId } = useParams();
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
|
|
||||||
@@ -88,14 +111,39 @@ export default function PoliciesPage() {
|
|||||||
const [showAddDialog, setShowAddDialog] = useState(false);
|
const [showAddDialog, setShowAddDialog] = useState(false);
|
||||||
const [editingPolicy, setEditingPolicy] = useState<PolicyRow | null>(null);
|
const [editingPolicy, setEditingPolicy] = useState<PolicyRow | null>(null);
|
||||||
|
|
||||||
|
const [defaultRoleMappingMode, setDefaultRoleMappingMode] =
|
||||||
|
useState<RoleMappingMode>("fixedRoles");
|
||||||
|
const [defaultFixedRoleNames, setDefaultFixedRoleNames] = useState<
|
||||||
|
string[]
|
||||||
|
>([]);
|
||||||
|
const [defaultMappingBuilderClaimPath, setDefaultMappingBuilderClaimPath] =
|
||||||
|
useState("groups");
|
||||||
|
const [defaultMappingBuilderRules, setDefaultMappingBuilderRules] =
|
||||||
|
useState<MappingBuilderRule[]>([createMappingBuilderRule()]);
|
||||||
|
const [defaultRawRoleExpression, setDefaultRawRoleExpression] =
|
||||||
|
useState("");
|
||||||
|
|
||||||
|
const [policyRoleMappingMode, setPolicyRoleMappingMode] =
|
||||||
|
useState<RoleMappingMode>("fixedRoles");
|
||||||
|
const [policyFixedRoleNames, setPolicyFixedRoleNames] = useState<string[]>(
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
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({
|
const policyFormSchema = z.object({
|
||||||
orgId: z.string().min(1, { message: t("orgRequired") }),
|
orgId: z.string().min(1, { message: t("orgRequired") }),
|
||||||
roleMapping: z.string().optional(),
|
|
||||||
orgMapping: z.string().optional()
|
orgMapping: z.string().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
const defaultMappingsSchema = z.object({
|
const defaultMappingsSchema = z.object({
|
||||||
defaultRoleMapping: z.string().optional(),
|
|
||||||
defaultOrgMapping: z.string().optional()
|
defaultOrgMapping: z.string().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -106,15 +154,15 @@ export default function PoliciesPage() {
|
|||||||
resolver: zodResolver(policyFormSchema),
|
resolver: zodResolver(policyFormSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
orgId: "",
|
orgId: "",
|
||||||
roleMapping: "",
|
|
||||||
orgMapping: ""
|
orgMapping: ""
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const policyFormOrgId = form.watch("orgId");
|
||||||
|
|
||||||
const defaultMappingsForm = useForm({
|
const defaultMappingsForm = useForm({
|
||||||
resolver: zodResolver(defaultMappingsSchema),
|
resolver: zodResolver(defaultMappingsSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
defaultRoleMapping: "",
|
|
||||||
defaultOrgMapping: ""
|
defaultOrgMapping: ""
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -127,9 +175,16 @@ export default function PoliciesPage() {
|
|||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
const data = res.data.data;
|
const data = res.data.data;
|
||||||
defaultMappingsForm.reset({
|
defaultMappingsForm.reset({
|
||||||
defaultRoleMapping: data.idp.defaultRoleMapping || "",
|
|
||||||
defaultOrgMapping: data.idp.defaultOrgMapping || ""
|
defaultOrgMapping: data.idp.defaultOrgMapping || ""
|
||||||
});
|
});
|
||||||
|
resetRoleMappingStateFromDetected(
|
||||||
|
setDefaultRoleMappingMode,
|
||||||
|
setDefaultFixedRoleNames,
|
||||||
|
setDefaultMappingBuilderClaimPath,
|
||||||
|
setDefaultMappingBuilderRules,
|
||||||
|
setDefaultRawRoleExpression,
|
||||||
|
data.idp.defaultRoleMapping
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast({
|
toast({
|
||||||
@@ -184,11 +239,67 @@ export default function PoliciesPage() {
|
|||||||
load();
|
load();
|
||||||
}, [idpId]);
|
}, [idpId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!showAddDialog) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const orgId = editingPolicy?.orgId || policyFormOrgId;
|
||||||
|
if (!orgId) {
|
||||||
|
setPolicyOrgRoles([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cancelled = false;
|
||||||
|
(async () => {
|
||||||
|
const res = await api
|
||||||
|
.get<AxiosResponse<ListRolesResponse>>(`/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 onAddPolicy = async (data: PolicyFormValues) => {
|
||||||
|
const roleMappingExpression = compileRoleMappingExpression({
|
||||||
|
mode: policyRoleMappingMode,
|
||||||
|
fixedRoleNames: policyFixedRoleNames,
|
||||||
|
mappingBuilder: {
|
||||||
|
claimPath: policyMappingBuilderClaimPath,
|
||||||
|
rules: policyMappingBuilderRules
|
||||||
|
},
|
||||||
|
rawExpression: policyRawRoleExpression
|
||||||
|
});
|
||||||
|
|
||||||
setAddPolicyLoading(true);
|
setAddPolicyLoading(true);
|
||||||
try {
|
try {
|
||||||
const res = await api.put(`/idp/${idpId}/org/${data.orgId}`, {
|
const res = await api.put(`/idp/${idpId}/org/${data.orgId}`, {
|
||||||
roleMapping: data.roleMapping,
|
roleMapping: roleMappingExpression,
|
||||||
orgMapping: data.orgMapping
|
orgMapping: data.orgMapping
|
||||||
});
|
});
|
||||||
if (res.status === 201) {
|
if (res.status === 201) {
|
||||||
@@ -197,7 +308,7 @@ export default function PoliciesPage() {
|
|||||||
name:
|
name:
|
||||||
organizations.find((org) => org.orgId === data.orgId)
|
organizations.find((org) => org.orgId === data.orgId)
|
||||||
?.name || "",
|
?.name || "",
|
||||||
roleMapping: data.roleMapping,
|
roleMapping: roleMappingExpression,
|
||||||
orgMapping: data.orgMapping
|
orgMapping: data.orgMapping
|
||||||
};
|
};
|
||||||
setPolicies([...policies, newPolicy]);
|
setPolicies([...policies, newPolicy]);
|
||||||
@@ -207,6 +318,7 @@ export default function PoliciesPage() {
|
|||||||
});
|
});
|
||||||
setShowAddDialog(false);
|
setShowAddDialog(false);
|
||||||
form.reset();
|
form.reset();
|
||||||
|
resetPolicyDialogRoleMappingState();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast({
|
toast({
|
||||||
@@ -222,12 +334,22 @@ export default function PoliciesPage() {
|
|||||||
const onEditPolicy = async (data: PolicyFormValues) => {
|
const onEditPolicy = async (data: PolicyFormValues) => {
|
||||||
if (!editingPolicy) return;
|
if (!editingPolicy) return;
|
||||||
|
|
||||||
|
const roleMappingExpression = compileRoleMappingExpression({
|
||||||
|
mode: policyRoleMappingMode,
|
||||||
|
fixedRoleNames: policyFixedRoleNames,
|
||||||
|
mappingBuilder: {
|
||||||
|
claimPath: policyMappingBuilderClaimPath,
|
||||||
|
rules: policyMappingBuilderRules
|
||||||
|
},
|
||||||
|
rawExpression: policyRawRoleExpression
|
||||||
|
});
|
||||||
|
|
||||||
setEditPolicyLoading(true);
|
setEditPolicyLoading(true);
|
||||||
try {
|
try {
|
||||||
const res = await api.post(
|
const res = await api.post(
|
||||||
`/idp/${idpId}/org/${editingPolicy.orgId}`,
|
`/idp/${idpId}/org/${editingPolicy.orgId}`,
|
||||||
{
|
{
|
||||||
roleMapping: data.roleMapping,
|
roleMapping: roleMappingExpression,
|
||||||
orgMapping: data.orgMapping
|
orgMapping: data.orgMapping
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -237,7 +359,7 @@ export default function PoliciesPage() {
|
|||||||
policy.orgId === editingPolicy.orgId
|
policy.orgId === editingPolicy.orgId
|
||||||
? {
|
? {
|
||||||
...policy,
|
...policy,
|
||||||
roleMapping: data.roleMapping,
|
roleMapping: roleMappingExpression,
|
||||||
orgMapping: data.orgMapping
|
orgMapping: data.orgMapping
|
||||||
}
|
}
|
||||||
: policy
|
: policy
|
||||||
@@ -250,6 +372,7 @@ export default function PoliciesPage() {
|
|||||||
setShowAddDialog(false);
|
setShowAddDialog(false);
|
||||||
setEditingPolicy(null);
|
setEditingPolicy(null);
|
||||||
form.reset();
|
form.reset();
|
||||||
|
resetPolicyDialogRoleMappingState();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast({
|
toast({
|
||||||
@@ -287,10 +410,20 @@ export default function PoliciesPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onUpdateDefaultMappings = async (data: DefaultMappingsValues) => {
|
const onUpdateDefaultMappings = async (data: DefaultMappingsValues) => {
|
||||||
|
const defaultRoleMappingExpression = compileRoleMappingExpression({
|
||||||
|
mode: defaultRoleMappingMode,
|
||||||
|
fixedRoleNames: defaultFixedRoleNames,
|
||||||
|
mappingBuilder: {
|
||||||
|
claimPath: defaultMappingBuilderClaimPath,
|
||||||
|
rules: defaultMappingBuilderRules
|
||||||
|
},
|
||||||
|
rawExpression: defaultRawRoleExpression
|
||||||
|
});
|
||||||
|
|
||||||
setUpdateDefaultMappingsLoading(true);
|
setUpdateDefaultMappingsLoading(true);
|
||||||
try {
|
try {
|
||||||
const res = await api.post(`/idp/${idpId}/oidc`, {
|
const res = await api.post(`/idp/${idpId}/oidc`, {
|
||||||
defaultRoleMapping: data.defaultRoleMapping,
|
defaultRoleMapping: defaultRoleMappingExpression,
|
||||||
defaultOrgMapping: data.defaultOrgMapping
|
defaultOrgMapping: data.defaultOrgMapping
|
||||||
});
|
});
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
@@ -317,25 +450,36 @@ export default function PoliciesPage() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SettingsContainer>
|
<SettingsContainer>
|
||||||
<Alert variant="neutral" className="mb-6">
|
<PolicyTable
|
||||||
<InfoIcon className="h-4 w-4" />
|
policies={policies}
|
||||||
<AlertTitle className="font-semibold">
|
onDelete={onDeletePolicy}
|
||||||
{t("orgPoliciesAbout")}
|
onAdd={() => {
|
||||||
</AlertTitle>
|
loadOrganizations();
|
||||||
<AlertDescription>
|
form.reset({
|
||||||
{/*TODO(vlalx): Validate replacing */}
|
orgId: "",
|
||||||
{t("orgPoliciesAboutDescription")}{" "}
|
orgMapping: ""
|
||||||
<Link
|
});
|
||||||
href="https://docs.pangolin.net/manage/identity-providers/auto-provisioning"
|
setEditingPolicy(null);
|
||||||
target="_blank"
|
resetPolicyDialogRoleMappingState();
|
||||||
rel="noopener noreferrer"
|
setShowAddDialog(true);
|
||||||
className="text-primary hover:underline"
|
}}
|
||||||
>
|
onEdit={(policy) => {
|
||||||
{t("orgPoliciesAboutDescriptionLink")}
|
setEditingPolicy(policy);
|
||||||
<ExternalLink className="ml-1 h-4 w-4 inline" />
|
form.reset({
|
||||||
</Link>
|
orgId: policy.orgId,
|
||||||
</AlertDescription>
|
orgMapping: policy.orgMapping || ""
|
||||||
</Alert>
|
});
|
||||||
|
resetRoleMappingStateFromDetected(
|
||||||
|
setPolicyRoleMappingMode,
|
||||||
|
setPolicyFixedRoleNames,
|
||||||
|
setPolicyMappingBuilderClaimPath,
|
||||||
|
setPolicyMappingBuilderRules,
|
||||||
|
setPolicyRawRoleExpression,
|
||||||
|
policy.roleMapping
|
||||||
|
);
|
||||||
|
setShowAddDialog(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
<SettingsSection>
|
<SettingsSection>
|
||||||
<SettingsSectionHeader>
|
<SettingsSectionHeader>
|
||||||
@@ -353,51 +497,58 @@ export default function PoliciesPage() {
|
|||||||
onUpdateDefaultMappings
|
onUpdateDefaultMappings
|
||||||
)}
|
)}
|
||||||
id="policy-default-mappings-form"
|
id="policy-default-mappings-form"
|
||||||
className="space-y-4"
|
className="space-y-6"
|
||||||
>
|
>
|
||||||
<div className="grid gap-6 md:grid-cols-2">
|
<RoleMappingConfigFields
|
||||||
<FormField
|
fieldIdPrefix="admin-idp-default-role"
|
||||||
control={defaultMappingsForm.control}
|
showFreeformRoleNamesHint={true}
|
||||||
name="defaultRoleMapping"
|
roleMappingMode={defaultRoleMappingMode}
|
||||||
render={({ field }) => (
|
onRoleMappingModeChange={
|
||||||
<FormItem>
|
setDefaultRoleMappingMode
|
||||||
<FormLabel>
|
}
|
||||||
{t("defaultMappingsRole")}
|
roles={[]}
|
||||||
</FormLabel>
|
fixedRoleNames={defaultFixedRoleNames}
|
||||||
<FormControl>
|
onFixedRoleNamesChange={
|
||||||
<Input {...field} />
|
setDefaultFixedRoleNames
|
||||||
</FormControl>
|
}
|
||||||
<FormDescription>
|
mappingBuilderClaimPath={
|
||||||
{t(
|
defaultMappingBuilderClaimPath
|
||||||
"defaultMappingsRoleDescription"
|
}
|
||||||
)}
|
onMappingBuilderClaimPathChange={
|
||||||
</FormDescription>
|
setDefaultMappingBuilderClaimPath
|
||||||
<FormMessage />
|
}
|
||||||
</FormItem>
|
mappingBuilderRules={
|
||||||
)}
|
defaultMappingBuilderRules
|
||||||
/>
|
}
|
||||||
|
onMappingBuilderRulesChange={
|
||||||
|
setDefaultMappingBuilderRules
|
||||||
|
}
|
||||||
|
rawExpression={defaultRawRoleExpression}
|
||||||
|
onRawExpressionChange={
|
||||||
|
setDefaultRawRoleExpression
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={defaultMappingsForm.control}
|
control={defaultMappingsForm.control}
|
||||||
name="defaultOrgMapping"
|
name="defaultOrgMapping"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
{t("defaultMappingsOrg")}
|
{t("defaultMappingsOrg")}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} />
|
<Input {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
{t(
|
{t(
|
||||||
"defaultMappingsOrgDescription"
|
"defaultMappingsOrgDescription"
|
||||||
)}
|
)}
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
<SettingsSectionFooter>
|
<SettingsSectionFooter>
|
||||||
@@ -411,41 +562,20 @@ export default function PoliciesPage() {
|
|||||||
</SettingsSectionFooter>
|
</SettingsSectionFooter>
|
||||||
</SettingsSectionBody>
|
</SettingsSectionBody>
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
|
|
||||||
<PolicyTable
|
|
||||||
policies={policies}
|
|
||||||
onDelete={onDeletePolicy}
|
|
||||||
onAdd={() => {
|
|
||||||
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);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</SettingsContainer>
|
</SettingsContainer>
|
||||||
|
|
||||||
<Credenza
|
<Credenza
|
||||||
open={showAddDialog}
|
open={showAddDialog}
|
||||||
onOpenChange={(val) => {
|
onOpenChange={(val) => {
|
||||||
setShowAddDialog(val);
|
setShowAddDialog(val);
|
||||||
setEditingPolicy(null);
|
if (!val) {
|
||||||
form.reset();
|
setEditingPolicy(null);
|
||||||
|
form.reset();
|
||||||
|
resetPolicyDialogRoleMappingState();
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CredenzaContent>
|
<CredenzaContent className="max-w-4xl w-[calc(100vw-2rem)] sm:w-full">
|
||||||
<CredenzaHeader>
|
<CredenzaHeader>
|
||||||
<CredenzaTitle>
|
<CredenzaTitle>
|
||||||
{editingPolicy
|
{editingPolicy
|
||||||
@@ -456,7 +586,7 @@ export default function PoliciesPage() {
|
|||||||
{t("orgPolicyConfig")}
|
{t("orgPolicyConfig")}
|
||||||
</CredenzaDescription>
|
</CredenzaDescription>
|
||||||
</CredenzaHeader>
|
</CredenzaHeader>
|
||||||
<CredenzaBody>
|
<CredenzaBody className="min-w-0 overflow-x-auto">
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
onSubmit={form.handleSubmit(
|
onSubmit={form.handleSubmit(
|
||||||
@@ -557,25 +687,34 @@ export default function PoliciesPage() {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormField
|
<RoleMappingConfigFields
|
||||||
control={form.control}
|
fieldIdPrefix="admin-idp-policy-role"
|
||||||
name="roleMapping"
|
showFreeformRoleNamesHint={false}
|
||||||
render={({ field }) => (
|
roleMappingMode={policyRoleMappingMode}
|
||||||
<FormItem>
|
onRoleMappingModeChange={
|
||||||
<FormLabel>
|
setPolicyRoleMappingMode
|
||||||
{t("roleMappingPathOptional")}
|
}
|
||||||
</FormLabel>
|
roles={policyOrgRoles}
|
||||||
<FormControl>
|
fixedRoleNames={policyFixedRoleNames}
|
||||||
<Input {...field} />
|
onFixedRoleNamesChange={
|
||||||
</FormControl>
|
setPolicyFixedRoleNames
|
||||||
<FormDescription>
|
}
|
||||||
{t(
|
mappingBuilderClaimPath={
|
||||||
"defaultMappingsRoleDescription"
|
policyMappingBuilderClaimPath
|
||||||
)}
|
}
|
||||||
</FormDescription>
|
onMappingBuilderClaimPathChange={
|
||||||
<FormMessage />
|
setPolicyMappingBuilderClaimPath
|
||||||
</FormItem>
|
}
|
||||||
)}
|
mappingBuilderRules={
|
||||||
|
policyMappingBuilderRules
|
||||||
|
}
|
||||||
|
onMappingBuilderRulesChange={
|
||||||
|
setPolicyMappingBuilderRules
|
||||||
|
}
|
||||||
|
rawExpression={policyRawRoleExpression}
|
||||||
|
onRawExpressionChange={
|
||||||
|
setPolicyRawRoleExpression
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
|
|||||||
@@ -340,16 +340,6 @@ export default function Page() {
|
|||||||
/>
|
/>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
|
|
||||||
<Alert variant="neutral">
|
|
||||||
<InfoIcon className="h-4 w-4" />
|
|
||||||
<AlertTitle className="font-semibold">
|
|
||||||
{t("idpOidcConfigureAlert")}
|
|
||||||
</AlertTitle>
|
|
||||||
<AlertDescription>
|
|
||||||
{t("idpOidcConfigureAlertDescription")}
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
</SettingsSectionBody>
|
</SettingsSectionBody>
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
|
|
||||||
@@ -369,29 +359,6 @@ export default function Page() {
|
|||||||
id="create-idp-form"
|
id="create-idp-form"
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
>
|
>
|
||||||
<Alert variant="neutral">
|
|
||||||
<InfoIcon className="h-4 w-4" />
|
|
||||||
<AlertTitle className="font-semibold">
|
|
||||||
{t("idpJmespathAbout")}
|
|
||||||
</AlertTitle>
|
|
||||||
<AlertDescription>
|
|
||||||
{t(
|
|
||||||
"idpJmespathAboutDescription"
|
|
||||||
)}{" "}
|
|
||||||
<a
|
|
||||||
href="https://jmespath.org"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="text-primary hover:underline inline-flex items-center"
|
|
||||||
>
|
|
||||||
{t(
|
|
||||||
"idpJmespathAboutDescriptionLink"
|
|
||||||
)}{" "}
|
|
||||||
<ExternalLink className="ml-1 h-4 w-4" />
|
|
||||||
</a>
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="identifierPath"
|
name="identifierPath"
|
||||||
|
|||||||
@@ -1,23 +1,15 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import {
|
import { FormDescription } from "@app/components/ui/form";
|
||||||
FormLabel,
|
|
||||||
FormDescription
|
|
||||||
} from "@app/components/ui/form";
|
|
||||||
import { SwitchInput } from "@app/components/SwitchInput";
|
import { SwitchInput } from "@app/components/SwitchInput";
|
||||||
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 { useTranslations } from "next-intl";
|
||||||
import { useMemo, useState } from "react";
|
|
||||||
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
||||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||||
import { Tag, TagInput } from "@app/components/tags/tag-input";
|
|
||||||
import {
|
import {
|
||||||
createMappingBuilderRule,
|
|
||||||
MappingBuilderRule,
|
MappingBuilderRule,
|
||||||
RoleMappingMode
|
RoleMappingMode
|
||||||
} from "@app/lib/idpRoleMapping";
|
} from "@app/lib/idpRoleMapping";
|
||||||
|
import RoleMappingConfigFields from "@app/components/RoleMappingConfigFields";
|
||||||
|
|
||||||
type Role = {
|
type Role = {
|
||||||
roleId: number;
|
roleId: number;
|
||||||
@@ -57,18 +49,6 @@ export default function AutoProvisionConfigWidget({
|
|||||||
}: AutoProvisionConfigWidgetProps) {
|
}: AutoProvisionConfigWidgetProps) {
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
const { isPaidUser } = usePaidStatus();
|
const { isPaidUser } = usePaidStatus();
|
||||||
const [activeFixedRoleTagIndex, setActiveFixedRoleTagIndex] = useState<
|
|
||||||
number | null
|
|
||||||
>(null);
|
|
||||||
|
|
||||||
const roleOptions = useMemo(
|
|
||||||
() =>
|
|
||||||
roles.map((role) => ({
|
|
||||||
id: role.name,
|
|
||||||
text: role.name
|
|
||||||
})),
|
|
||||||
[roles]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
@@ -80,261 +60,30 @@ export default function AutoProvisionConfigWidget({
|
|||||||
onCheckedChange={onAutoProvisionChange}
|
onCheckedChange={onAutoProvisionChange}
|
||||||
disabled={!isPaidUser(tierMatrix.autoProvisioning)}
|
disabled={!isPaidUser(tierMatrix.autoProvisioning)}
|
||||||
/>
|
/>
|
||||||
<span className="text-sm text-muted-foreground">
|
<FormDescription className="text-sm text-muted-foreground">
|
||||||
{t("idpAutoProvisionUsersDescription")}
|
{t("idpAutoProvisionUsersDescription")}
|
||||||
</span>
|
</FormDescription>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{autoProvision && (
|
{autoProvision && (
|
||||||
<div className="space-y-4">
|
<RoleMappingConfigFields
|
||||||
<div>
|
fieldIdPrefix="org-idp-auto-provision"
|
||||||
<FormLabel className="mb-2">
|
showFreeformRoleNamesHint={false}
|
||||||
{t("roleMapping")}
|
roleMappingMode={roleMappingMode}
|
||||||
</FormLabel>
|
onRoleMappingModeChange={onRoleMappingModeChange}
|
||||||
<FormDescription className="mb-4">
|
roles={roles}
|
||||||
{t("roleMappingDescription")}
|
fixedRoleNames={fixedRoleNames}
|
||||||
</FormDescription>
|
onFixedRoleNamesChange={onFixedRoleNamesChange}
|
||||||
|
mappingBuilderClaimPath={mappingBuilderClaimPath}
|
||||||
<RadioGroup
|
onMappingBuilderClaimPathChange={
|
||||||
value={roleMappingMode}
|
onMappingBuilderClaimPathChange
|
||||||
onValueChange={onRoleMappingModeChange}
|
}
|
||||||
className="flex flex-wrap gap-x-6 gap-y-2"
|
mappingBuilderRules={mappingBuilderRules}
|
||||||
>
|
onMappingBuilderRulesChange={onMappingBuilderRulesChange}
|
||||||
<div className="flex items-center space-x-2">
|
rawExpression={rawExpression}
|
||||||
<RadioGroupItem
|
onRawExpressionChange={onRawExpressionChange}
|
||||||
value="fixedRoles"
|
/>
|
||||||
id="fixed-roles-mode"
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
htmlFor="fixed-roles-mode"
|
|
||||||
className="text-sm font-medium"
|
|
||||||
>
|
|
||||||
Fixed roles
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<RadioGroupItem
|
|
||||||
value="mappingBuilder"
|
|
||||||
id="mapping-builder-mode"
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
htmlFor="mapping-builder-mode"
|
|
||||||
className="text-sm font-medium"
|
|
||||||
>
|
|
||||||
Mapping builder
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<RadioGroupItem
|
|
||||||
value="rawExpression"
|
|
||||||
id="expression-mode"
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
htmlFor="expression-mode"
|
|
||||||
className="text-sm font-medium"
|
|
||||||
>
|
|
||||||
Raw expression
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</RadioGroup>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{roleMappingMode === "fixedRoles" && (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<TagInput
|
|
||||||
tags={fixedRoleNames.map((name) => ({
|
|
||||||
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"
|
|
||||||
/>
|
|
||||||
<FormDescription>
|
|
||||||
Assign the same role set to every auto-provisioned
|
|
||||||
user.
|
|
||||||
</FormDescription>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{roleMappingMode === "mappingBuilder" && (
|
|
||||||
<div className="space-y-4 rounded-md border p-3">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<FormLabel>Claim path</FormLabel>
|
|
||||||
<Input
|
|
||||||
value={mappingBuilderClaimPath}
|
|
||||||
onChange={(e) =>
|
|
||||||
onMappingBuilderClaimPathChange(
|
|
||||||
e.target.value
|
|
||||||
)
|
|
||||||
}
|
|
||||||
placeholder="groups"
|
|
||||||
/>
|
|
||||||
<FormDescription>
|
|
||||||
Path in the token payload that contains source
|
|
||||||
values (for example, groups).
|
|
||||||
</FormDescription>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-3">
|
|
||||||
<div className="hidden md:grid md:grid-cols-[minmax(220px,1fr)_minmax(340px,2fr)_auto] md:gap-3">
|
|
||||||
<FormLabel>Match value</FormLabel>
|
|
||||||
<FormLabel>Assign roles</FormLabel>
|
|
||||||
<span />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{mappingBuilderRules.map((rule, index) => (
|
|
||||||
<BuilderRuleRow
|
|
||||||
key={rule.id ?? `mapping-rule-${index}`}
|
|
||||||
roleOptions={roleOptions}
|
|
||||||
rule={rule}
|
|
||||||
onChange={(nextRule) => {
|
|
||||||
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()]
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => {
|
|
||||||
onMappingBuilderRulesChange([
|
|
||||||
...mappingBuilderRules,
|
|
||||||
createMappingBuilderRule()
|
|
||||||
]);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Add mapping rule
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{roleMappingMode === "rawExpression" && (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Input
|
|
||||||
value={rawExpression}
|
|
||||||
onChange={(e) =>
|
|
||||||
onRawExpressionChange(e.target.value)
|
|
||||||
}
|
|
||||||
placeholder={t("roleMappingExpressionPlaceholder")}
|
|
||||||
/>
|
|
||||||
<FormDescription>
|
|
||||||
Expression must evaluate to a string or string
|
|
||||||
array.
|
|
||||||
</FormDescription>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function BuilderRuleRow({
|
|
||||||
rule,
|
|
||||||
roleOptions,
|
|
||||||
onChange,
|
|
||||||
onRemove
|
|
||||||
}: {
|
|
||||||
rule: MappingBuilderRule;
|
|
||||||
roleOptions: Tag[];
|
|
||||||
onChange: (rule: MappingBuilderRule) => void;
|
|
||||||
onRemove: () => void;
|
|
||||||
}) {
|
|
||||||
const [activeTagIndex, setActiveTagIndex] = useState<number | null>(null);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="grid gap-3 rounded-md border p-3 md:grid-cols-[minmax(220px,1fr)_minmax(340px,2fr)_auto] md:items-start">
|
|
||||||
<div className="space-y-1">
|
|
||||||
<FormLabel className="text-xs md:hidden">Match value</FormLabel>
|
|
||||||
<Input
|
|
||||||
value={rule.matchValue}
|
|
||||||
onChange={(e) =>
|
|
||||||
onChange({
|
|
||||||
...rule,
|
|
||||||
matchValue: e.target.value
|
|
||||||
})
|
|
||||||
}
|
|
||||||
placeholder="Match value (for example: admin)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-1 min-w-0">
|
|
||||||
<FormLabel className="text-xs md:hidden">Assign roles</FormLabel>
|
|
||||||
<TagInput
|
|
||||||
tags={rule.roleNames.map((name) => ({ 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"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-end md:justify-start">
|
|
||||||
<Button type="button" variant="ghost" onClick={onRemove}>
|
|
||||||
Remove
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
366
src/components/RoleMappingConfigFields.tsx
Normal file
366
src/components/RoleMappingConfigFields.tsx
Normal file
@@ -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 (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<FormLabel className="mb-2">{t("roleMapping")}</FormLabel>
|
||||||
|
<FormDescription className="mb-4">
|
||||||
|
{t("roleMappingDescription")}
|
||||||
|
</FormDescription>
|
||||||
|
|
||||||
|
<RadioGroup
|
||||||
|
value={roleMappingMode}
|
||||||
|
onValueChange={onRoleMappingModeChange}
|
||||||
|
className="flex flex-wrap gap-x-6 gap-y-2"
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<RadioGroupItem value="fixedRoles" id={fixedRadioId} />
|
||||||
|
<label
|
||||||
|
htmlFor={fixedRadioId}
|
||||||
|
className="text-sm font-medium"
|
||||||
|
>
|
||||||
|
{t("roleMappingModeFixedRoles")}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<RadioGroupItem
|
||||||
|
value="mappingBuilder"
|
||||||
|
id={builderRadioId}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
htmlFor={builderRadioId}
|
||||||
|
className="text-sm font-medium"
|
||||||
|
>
|
||||||
|
{t("roleMappingModeMappingBuilder")}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<RadioGroupItem value="rawExpression" id={rawRadioId} />
|
||||||
|
<label
|
||||||
|
htmlFor={rawRadioId}
|
||||||
|
className="text-sm font-medium"
|
||||||
|
>
|
||||||
|
{t("roleMappingModeRawExpression")}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</RadioGroup>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{roleMappingMode === "fixedRoles" && (
|
||||||
|
<div className="space-y-2 min-w-0 max-w-full">
|
||||||
|
<TagInput
|
||||||
|
tags={fixedRoleNames.map((name) => ({
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
<FormDescription>
|
||||||
|
{showFreeformRoleNamesHint
|
||||||
|
? t("roleMappingFixedRolesDescriptionDefaultPolicy")
|
||||||
|
: t("roleMappingFixedRolesDescriptionSameForAll")}
|
||||||
|
</FormDescription>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{roleMappingMode === "mappingBuilder" && (
|
||||||
|
<div className="space-y-4 rounded-md border p-3 min-w-0 max-w-full">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<FormLabel>{t("roleMappingClaimPath")}</FormLabel>
|
||||||
|
<Input
|
||||||
|
value={mappingBuilderClaimPath}
|
||||||
|
onChange={(e) =>
|
||||||
|
onMappingBuilderClaimPathChange(e.target.value)
|
||||||
|
}
|
||||||
|
placeholder={t("roleMappingClaimPathPlaceholder")}
|
||||||
|
/>
|
||||||
|
<FormDescription>
|
||||||
|
{t("roleMappingClaimPathDescription")}
|
||||||
|
</FormDescription>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div
|
||||||
|
className={`hidden ${mappingRulesGridClass} md:items-end`}
|
||||||
|
>
|
||||||
|
<FormLabel className="min-w-0">
|
||||||
|
{t("roleMappingMatchValue")}
|
||||||
|
</FormLabel>
|
||||||
|
<FormLabel className="min-w-0">
|
||||||
|
{t("roleMappingAssignRoles")}
|
||||||
|
</FormLabel>
|
||||||
|
<span aria-hidden className="min-w-0" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{mappingBuilderRules.map((rule, index) => (
|
||||||
|
<BuilderRuleRow
|
||||||
|
key={rule.id ?? `mapping-rule-${index}`}
|
||||||
|
mappingRulesGridClass={mappingRulesGridClass}
|
||||||
|
fieldIdPrefix={`${fieldIdPrefix}-rule-${index}`}
|
||||||
|
roleOptions={roleOptions}
|
||||||
|
restrictToOrgRoles={restrictToOrgRoles}
|
||||||
|
showFreeformRoleNamesHint={
|
||||||
|
showFreeformRoleNamesHint
|
||||||
|
}
|
||||||
|
rule={rule}
|
||||||
|
onChange={(nextRule) => {
|
||||||
|
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()]
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
onMappingBuilderRulesChange([
|
||||||
|
...mappingBuilderRules,
|
||||||
|
createMappingBuilderRule()
|
||||||
|
]);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("roleMappingAddMappingRule")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{roleMappingMode === "rawExpression" && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Input
|
||||||
|
value={rawExpression}
|
||||||
|
onChange={(e) => onRawExpressionChange(e.target.value)}
|
||||||
|
placeholder={t("roleMappingExpressionPlaceholder")}
|
||||||
|
/>
|
||||||
|
<FormDescription>
|
||||||
|
{t("roleMappingRawExpressionResultDescription")}
|
||||||
|
</FormDescription>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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<number | null>(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`grid gap-3 min-w-0 ${mappingRulesGridClass} md:items-start`}
|
||||||
|
>
|
||||||
|
<div className="space-y-1 min-w-0">
|
||||||
|
<FormLabel className="text-xs md:hidden">
|
||||||
|
{t("roleMappingMatchValue")}
|
||||||
|
</FormLabel>
|
||||||
|
<Input
|
||||||
|
id={`${fieldIdPrefix}-match`}
|
||||||
|
value={rule.matchValue}
|
||||||
|
onChange={(e) =>
|
||||||
|
onChange({
|
||||||
|
...rule,
|
||||||
|
matchValue: e.target.value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
placeholder={t("roleMappingMatchValuePlaceholder")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1 min-w-0 w-full max-w-full">
|
||||||
|
<FormLabel className="text-xs md:hidden">
|
||||||
|
{t("roleMappingAssignRoles")}
|
||||||
|
</FormLabel>
|
||||||
|
<div className="min-w-0 max-w-full">
|
||||||
|
<TagInput
|
||||||
|
tags={rule.roleNames.map((name) => ({
|
||||||
|
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"
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{showFreeformRoleNamesHint && (
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{t("roleMappingBuilderFreeformRowHint")}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex min-w-0 justify-end md:justify-start md:pt-0">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
className="h-9 shrink-0 px-2"
|
||||||
|
onClick={onRemove}
|
||||||
|
>
|
||||||
|
{t("roleMappingRemoveRule")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user