mirror of
https://github.com/fosrl/pangolin.git
synced 2026-03-31 15:06:42 +00:00
respect full rbac feature in auto provisioning
This commit is contained in:
@@ -1956,6 +1956,7 @@
|
|||||||
"roleMappingAssignRoles": "Assign Roles",
|
"roleMappingAssignRoles": "Assign Roles",
|
||||||
"roleMappingAddMappingRule": "Add Mapping Rule",
|
"roleMappingAddMappingRule": "Add Mapping Rule",
|
||||||
"roleMappingRawExpressionResultDescription": "Expression must evaluate to a string or string array.",
|
"roleMappingRawExpressionResultDescription": "Expression must evaluate to a string or string array.",
|
||||||
|
"roleMappingRawExpressionResultDescriptionSingleRole": "Expression must evaluate to a string (a single role name).",
|
||||||
"roleMappingMatchValuePlaceholder": "Match value (for example: admin)",
|
"roleMappingMatchValuePlaceholder": "Match value (for example: admin)",
|
||||||
"roleMappingAssignRolesPlaceholderFreeform": "Type role names (exact per org)",
|
"roleMappingAssignRolesPlaceholderFreeform": "Type role names (exact per org)",
|
||||||
"roleMappingBuilderFreeformRowHint": "Role names must match a role in each target organization.",
|
"roleMappingBuilderFreeformRowHint": "Role names must match a role in each target organization.",
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ export enum TierFeature {
|
|||||||
SessionDurationPolicies = "sessionDurationPolicies", // handle downgrade by setting to default duration
|
SessionDurationPolicies = "sessionDurationPolicies", // handle downgrade by setting to default duration
|
||||||
PasswordExpirationPolicies = "passwordExpirationPolicies", // handle downgrade by setting to default duration
|
PasswordExpirationPolicies = "passwordExpirationPolicies", // handle downgrade by setting to default duration
|
||||||
AutoProvisioning = "autoProvisioning", // handle downgrade by disabling auto provisioning
|
AutoProvisioning = "autoProvisioning", // handle downgrade by disabling auto provisioning
|
||||||
SshPam = "sshPam"
|
SshPam = "sshPam",
|
||||||
|
FullRbac = "fullRbac"
|
||||||
}
|
}
|
||||||
|
|
||||||
export const tierMatrix: Record<TierFeature, Tier[]> = {
|
export const tierMatrix: Record<TierFeature, Tier[]> = {
|
||||||
@@ -48,5 +49,6 @@ export const tierMatrix: Record<TierFeature, Tier[]> = {
|
|||||||
"enterprise"
|
"enterprise"
|
||||||
],
|
],
|
||||||
[TierFeature.AutoProvisioning]: ["tier1", "tier3", "enterprise"],
|
[TierFeature.AutoProvisioning]: ["tier1", "tier3", "enterprise"],
|
||||||
[TierFeature.SshPam]: ["tier1", "tier3", "enterprise"]
|
[TierFeature.SshPam]: ["tier1", "tier3", "enterprise"],
|
||||||
|
[TierFeature.FullRbac]: ["tier1", "tier2", "tier3", "enterprise"]
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -26,9 +26,10 @@ import {
|
|||||||
orgs,
|
orgs,
|
||||||
resources,
|
resources,
|
||||||
roles,
|
roles,
|
||||||
siteResources
|
siteResources,
|
||||||
|
userOrgRoles
|
||||||
} from "@server/db";
|
} from "@server/db";
|
||||||
import { eq } from "drizzle-orm";
|
import { and, eq } from "drizzle-orm";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the maximum allowed retention days for a given tier
|
* Get the maximum allowed retention days for a given tier
|
||||||
@@ -291,6 +292,10 @@ async function disableFeature(
|
|||||||
await disableSshPam(orgId);
|
await disableSshPam(orgId);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case TierFeature.FullRbac:
|
||||||
|
await disableFullRbac(orgId);
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
logger.warn(
|
logger.warn(
|
||||||
`Unknown feature ${feature} for org ${orgId}, skipping`
|
`Unknown feature ${feature} for org ${orgId}, skipping`
|
||||||
@@ -326,6 +331,10 @@ async function disableSshPam(orgId: string): Promise<void> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function disableFullRbac(orgId: string): Promise<void> {
|
||||||
|
logger.info(`Disabled full RBAC for org ${orgId}`);
|
||||||
|
}
|
||||||
|
|
||||||
async function disableLoginPageBranding(orgId: string): Promise<void> {
|
async function disableLoginPageBranding(orgId: string): Promise<void> {
|
||||||
const [existingBranding] = await db
|
const [existingBranding] = await db
|
||||||
.select()
|
.select()
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import { usageService } from "@server/lib/billing/usageService";
|
|||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
import { calculateUserClientsForOrgs } from "@server/lib/calculateUserClientsForOrgs";
|
import { calculateUserClientsForOrgs } from "@server/lib/calculateUserClientsForOrgs";
|
||||||
import { isSubscribed } from "#dynamic/lib/isSubscribed";
|
import { isSubscribed } from "#dynamic/lib/isSubscribed";
|
||||||
|
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
|
||||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||||
import {
|
import {
|
||||||
assignUserToOrg,
|
assignUserToOrg,
|
||||||
@@ -415,7 +416,15 @@ export async function validateOidcCallback(
|
|||||||
roleMappingResult
|
roleMappingResult
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!roleNames.length) {
|
const supportsMultiRole = await isLicensedOrSubscribed(
|
||||||
|
org.orgId,
|
||||||
|
tierMatrix.fullRbac
|
||||||
|
);
|
||||||
|
const effectiveRoleNames = supportsMultiRole
|
||||||
|
? roleNames
|
||||||
|
: roleNames.slice(0, 1);
|
||||||
|
|
||||||
|
if (!effectiveRoleNames.length) {
|
||||||
logger.error("Role mapping returned no valid roles", {
|
logger.error("Role mapping returned no valid roles", {
|
||||||
roleMappingResult
|
roleMappingResult
|
||||||
});
|
});
|
||||||
@@ -428,14 +437,14 @@ export async function validateOidcCallback(
|
|||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
eq(roles.orgId, org.orgId),
|
eq(roles.orgId, org.orgId),
|
||||||
inArray(roles.name, roleNames)
|
inArray(roles.name, effectiveRoleNames)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!roleRes.length) {
|
if (!roleRes.length) {
|
||||||
logger.error("No mapped roles found in organization", {
|
logger.error("No mapped roles found in organization", {
|
||||||
orgId: org.orgId,
|
orgId: org.orgId,
|
||||||
roleNames
|
roleNames: effectiveRoleNames
|
||||||
});
|
});
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,10 +69,7 @@ export default function AccessControlsPage() {
|
|||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
const { isPaidUser, hasSaasSubscription, hasEnterpriseLicense } =
|
const { isPaidUser, hasSaasSubscription, hasEnterpriseLicense } =
|
||||||
usePaidStatus();
|
usePaidStatus();
|
||||||
const multiRoleFeatureTiers = Array.from(
|
const isPaid = isPaidUser(tierMatrix.fullRbac);
|
||||||
new Set([...tierMatrix.sshPam, ...tierMatrix.orgOidc])
|
|
||||||
);
|
|
||||||
const isPaid = isPaidUser(multiRoleFeatureTiers);
|
|
||||||
const supportsMultipleRolesPerUser = isPaid;
|
const supportsMultipleRolesPerUser = isPaid;
|
||||||
const showMultiRolePaywallMessage =
|
const showMultiRolePaywallMessage =
|
||||||
!env.flags.disableEnterpriseFeatures &&
|
!env.flags.disableEnterpriseFeatures &&
|
||||||
|
|||||||
@@ -5,13 +5,17 @@ import { RadioGroup, RadioGroupItem } from "@app/components/ui/radio-group";
|
|||||||
import { Button } from "@app/components/ui/button";
|
import { Button } from "@app/components/ui/button";
|
||||||
import { Input } from "@app/components/ui/input";
|
import { Input } from "@app/components/ui/input";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { Tag, TagInput } from "@app/components/tags/tag-input";
|
import { Tag, TagInput } from "@app/components/tags/tag-input";
|
||||||
import {
|
import {
|
||||||
createMappingBuilderRule,
|
createMappingBuilderRule,
|
||||||
MappingBuilderRule,
|
MappingBuilderRule,
|
||||||
RoleMappingMode
|
RoleMappingMode
|
||||||
} from "@app/lib/idpRoleMapping";
|
} from "@app/lib/idpRoleMapping";
|
||||||
|
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
||||||
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
|
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||||
|
import { build } from "@server/build";
|
||||||
|
|
||||||
export type RoleMappingRoleOption = {
|
export type RoleMappingRoleOption = {
|
||||||
roleId: number;
|
roleId: number;
|
||||||
@@ -52,10 +56,17 @@ export default function RoleMappingConfigFields({
|
|||||||
showFreeformRoleNamesHint = false
|
showFreeformRoleNamesHint = false
|
||||||
}: RoleMappingConfigFieldsProps) {
|
}: RoleMappingConfigFieldsProps) {
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
|
const { env } = useEnvContext();
|
||||||
|
const { isPaidUser } = usePaidStatus();
|
||||||
const [activeFixedRoleTagIndex, setActiveFixedRoleTagIndex] = useState<
|
const [activeFixedRoleTagIndex, setActiveFixedRoleTagIndex] = useState<
|
||||||
number | null
|
number | null
|
||||||
>(null);
|
>(null);
|
||||||
|
|
||||||
|
const supportsMultipleRolesPerUser = isPaidUser(tierMatrix.fullRbac);
|
||||||
|
const showSingleRoleDisclaimer =
|
||||||
|
!env.flags.disableEnterpriseFeatures &&
|
||||||
|
!isPaidUser(tierMatrix.fullRbac);
|
||||||
|
|
||||||
const restrictToOrgRoles = roles.length > 0;
|
const restrictToOrgRoles = roles.length > 0;
|
||||||
|
|
||||||
const roleOptions = useMemo(
|
const roleOptions = useMemo(
|
||||||
@@ -67,13 +78,40 @@ export default function RoleMappingConfigFields({
|
|||||||
[roles]
|
[roles]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
!supportsMultipleRolesPerUser &&
|
||||||
|
mappingBuilderRules.length > 1
|
||||||
|
) {
|
||||||
|
onMappingBuilderRulesChange([mappingBuilderRules[0]]);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
supportsMultipleRolesPerUser,
|
||||||
|
mappingBuilderRules,
|
||||||
|
onMappingBuilderRulesChange
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!supportsMultipleRolesPerUser && fixedRoleNames.length > 1) {
|
||||||
|
onFixedRoleNamesChange([fixedRoleNames[0]]);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
supportsMultipleRolesPerUser,
|
||||||
|
fixedRoleNames,
|
||||||
|
onFixedRoleNamesChange
|
||||||
|
]);
|
||||||
|
|
||||||
const fixedRadioId = `${fieldIdPrefix}-fixed-roles-mode`;
|
const fixedRadioId = `${fieldIdPrefix}-fixed-roles-mode`;
|
||||||
const builderRadioId = `${fieldIdPrefix}-mapping-builder-mode`;
|
const builderRadioId = `${fieldIdPrefix}-mapping-builder-mode`;
|
||||||
const rawRadioId = `${fieldIdPrefix}-raw-expression-mode`;
|
const rawRadioId = `${fieldIdPrefix}-raw-expression-mode`;
|
||||||
|
|
||||||
|
const mappingBuilderShowsRemoveColumn =
|
||||||
|
supportsMultipleRolesPerUser || mappingBuilderRules.length > 1;
|
||||||
|
|
||||||
/** Same template on header + rows so 1fr/1.75fr columns line up (auto third col differs per row otherwise). */
|
/** Same template on header + rows so 1fr/1.75fr columns line up (auto third col differs per row otherwise). */
|
||||||
const mappingRulesGridClass =
|
const mappingRulesGridClass = mappingBuilderShowsRemoveColumn
|
||||||
"md:grid md:grid-cols-[minmax(0,1fr)_minmax(0,1.75fr)_6rem] md:gap-x-3";
|
? "md:grid md:grid-cols-[minmax(0,1fr)_minmax(0,1.75fr)_6rem] md:gap-x-3"
|
||||||
|
: "md:grid md:grid-cols-[minmax(0,1fr)_minmax(0,1.75fr)] md:gap-x-3";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
@@ -119,6 +157,13 @@ export default function RoleMappingConfigFields({
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
|
{showSingleRoleDisclaimer && (
|
||||||
|
<FormDescription className="mt-3">
|
||||||
|
{build === "saas"
|
||||||
|
? t("singleRolePerUserPlanNotice")
|
||||||
|
: t("singleRolePerUserEditionNotice")}
|
||||||
|
</FormDescription>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{roleMappingMode === "fixedRoles" && (
|
{roleMappingMode === "fixedRoles" && (
|
||||||
@@ -129,19 +174,37 @@ export default function RoleMappingConfigFields({
|
|||||||
text: name
|
text: name
|
||||||
}))}
|
}))}
|
||||||
setTags={(nextTags) => {
|
setTags={(nextTags) => {
|
||||||
|
const prevTags = fixedRoleNames.map((name) => ({
|
||||||
|
id: name,
|
||||||
|
text: name
|
||||||
|
}));
|
||||||
const next =
|
const next =
|
||||||
typeof nextTags === "function"
|
typeof nextTags === "function"
|
||||||
? nextTags(
|
? nextTags(prevTags)
|
||||||
fixedRoleNames.map((name) => ({
|
|
||||||
id: name,
|
|
||||||
text: name
|
|
||||||
}))
|
|
||||||
)
|
|
||||||
: nextTags;
|
: nextTags;
|
||||||
|
|
||||||
onFixedRoleNamesChange([
|
let names = [
|
||||||
...new Set(next.map((tag) => tag.text))
|
...new Set(next.map((tag) => tag.text))
|
||||||
]);
|
];
|
||||||
|
|
||||||
|
if (!supportsMultipleRolesPerUser) {
|
||||||
|
if (
|
||||||
|
names.length === 0 &&
|
||||||
|
fixedRoleNames.length > 0
|
||||||
|
) {
|
||||||
|
onFixedRoleNamesChange([
|
||||||
|
fixedRoleNames[
|
||||||
|
fixedRoleNames.length - 1
|
||||||
|
]!
|
||||||
|
]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (names.length > 1) {
|
||||||
|
names = [names[names.length - 1]!];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onFixedRoleNamesChange(names);
|
||||||
}}
|
}}
|
||||||
activeTagIndex={activeFixedRoleTagIndex}
|
activeTagIndex={activeFixedRoleTagIndex}
|
||||||
setActiveTagIndex={setActiveFixedRoleTagIndex}
|
setActiveTagIndex={setActiveFixedRoleTagIndex}
|
||||||
@@ -191,7 +254,9 @@ export default function RoleMappingConfigFields({
|
|||||||
<FormLabel className="min-w-0">
|
<FormLabel className="min-w-0">
|
||||||
{t("roleMappingAssignRoles")}
|
{t("roleMappingAssignRoles")}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<span aria-hidden className="min-w-0" />
|
{mappingBuilderShowsRemoveColumn ? (
|
||||||
|
<span aria-hidden className="min-w-0" />
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{mappingBuilderRules.map((rule, index) => (
|
{mappingBuilderRules.map((rule, index) => (
|
||||||
@@ -204,6 +269,10 @@ export default function RoleMappingConfigFields({
|
|||||||
showFreeformRoleNamesHint={
|
showFreeformRoleNamesHint={
|
||||||
showFreeformRoleNamesHint
|
showFreeformRoleNamesHint
|
||||||
}
|
}
|
||||||
|
supportsMultipleRolesPerUser={
|
||||||
|
supportsMultipleRolesPerUser
|
||||||
|
}
|
||||||
|
showRemoveButton={mappingBuilderShowsRemoveColumn}
|
||||||
rule={rule}
|
rule={rule}
|
||||||
onChange={(nextRule) => {
|
onChange={(nextRule) => {
|
||||||
const nextRules = mappingBuilderRules.map(
|
const nextRules = mappingBuilderRules.map(
|
||||||
@@ -227,18 +296,20 @@ export default function RoleMappingConfigFields({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
{supportsMultipleRolesPerUser ? (
|
||||||
type="button"
|
<Button
|
||||||
variant="outline"
|
type="button"
|
||||||
onClick={() => {
|
variant="outline"
|
||||||
onMappingBuilderRulesChange([
|
onClick={() => {
|
||||||
...mappingBuilderRules,
|
onMappingBuilderRulesChange([
|
||||||
createMappingBuilderRule()
|
...mappingBuilderRules,
|
||||||
]);
|
createMappingBuilderRule()
|
||||||
}}
|
]);
|
||||||
>
|
}}
|
||||||
{t("roleMappingAddMappingRule")}
|
>
|
||||||
</Button>
|
{t("roleMappingAddMappingRule")}
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -250,7 +321,11 @@ export default function RoleMappingConfigFields({
|
|||||||
placeholder={t("roleMappingExpressionPlaceholder")}
|
placeholder={t("roleMappingExpressionPlaceholder")}
|
||||||
/>
|
/>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
{t("roleMappingRawExpressionResultDescription")}
|
{supportsMultipleRolesPerUser
|
||||||
|
? t("roleMappingRawExpressionResultDescription")
|
||||||
|
: t(
|
||||||
|
"roleMappingRawExpressionResultDescriptionSingleRole"
|
||||||
|
)}
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -265,6 +340,8 @@ function BuilderRuleRow({
|
|||||||
showFreeformRoleNamesHint,
|
showFreeformRoleNamesHint,
|
||||||
fieldIdPrefix,
|
fieldIdPrefix,
|
||||||
mappingRulesGridClass,
|
mappingRulesGridClass,
|
||||||
|
supportsMultipleRolesPerUser,
|
||||||
|
showRemoveButton,
|
||||||
onChange,
|
onChange,
|
||||||
onRemove
|
onRemove
|
||||||
}: {
|
}: {
|
||||||
@@ -274,6 +351,8 @@ function BuilderRuleRow({
|
|||||||
showFreeformRoleNamesHint: boolean;
|
showFreeformRoleNamesHint: boolean;
|
||||||
fieldIdPrefix: string;
|
fieldIdPrefix: string;
|
||||||
mappingRulesGridClass: string;
|
mappingRulesGridClass: string;
|
||||||
|
supportsMultipleRolesPerUser: boolean;
|
||||||
|
showRemoveButton: boolean;
|
||||||
onChange: (rule: MappingBuilderRule) => void;
|
onChange: (rule: MappingBuilderRule) => void;
|
||||||
onRemove: () => void;
|
onRemove: () => void;
|
||||||
}) {
|
}) {
|
||||||
@@ -311,20 +390,44 @@ function BuilderRuleRow({
|
|||||||
text: name
|
text: name
|
||||||
}))}
|
}))}
|
||||||
setTags={(nextTags) => {
|
setTags={(nextTags) => {
|
||||||
|
const prevRoleTags = rule.roleNames.map(
|
||||||
|
(name) => ({
|
||||||
|
id: name,
|
||||||
|
text: name
|
||||||
|
})
|
||||||
|
);
|
||||||
const next =
|
const next =
|
||||||
typeof nextTags === "function"
|
typeof nextTags === "function"
|
||||||
? nextTags(
|
? nextTags(prevRoleTags)
|
||||||
rule.roleNames.map((name) => ({
|
|
||||||
id: name,
|
|
||||||
text: name
|
|
||||||
}))
|
|
||||||
)
|
|
||||||
: nextTags;
|
: nextTags;
|
||||||
|
|
||||||
|
let names = [
|
||||||
|
...new Set(next.map((tag) => tag.text))
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!supportsMultipleRolesPerUser) {
|
||||||
|
if (
|
||||||
|
names.length === 0 &&
|
||||||
|
rule.roleNames.length > 0
|
||||||
|
) {
|
||||||
|
onChange({
|
||||||
|
...rule,
|
||||||
|
roleNames: [
|
||||||
|
rule.roleNames[
|
||||||
|
rule.roleNames.length - 1
|
||||||
|
]!
|
||||||
|
]
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (names.length > 1) {
|
||||||
|
names = [names[names.length - 1]!];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onChange({
|
onChange({
|
||||||
...rule,
|
...rule,
|
||||||
roleNames: [
|
roleNames: names
|
||||||
...new Set(next.map((tag) => tag.text))
|
|
||||||
]
|
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
activeTagIndex={activeTagIndex}
|
activeTagIndex={activeTagIndex}
|
||||||
@@ -351,16 +454,18 @@ function BuilderRuleRow({
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex min-w-0 justify-end md:justify-start md:pt-0">
|
{showRemoveButton ? (
|
||||||
<Button
|
<div className="flex min-w-0 justify-end md:justify-start md:pt-0">
|
||||||
type="button"
|
<Button
|
||||||
variant="outline"
|
type="button"
|
||||||
className="h-9 shrink-0 px-2"
|
variant="outline"
|
||||||
onClick={onRemove}
|
className="h-9 shrink-0 px-2"
|
||||||
>
|
onClick={onRemove}
|
||||||
{t("roleMappingRemoveRule")}
|
>
|
||||||
</Button>
|
{t("roleMappingRemoveRule")}
|
||||||
</div>
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user