mirror of
https://github.com/fosrl/pangolin.git
synced 2026-03-29 01:45:44 +00:00
respect full rbac feature in auto provisioning
This commit is contained in:
@@ -1956,6 +1956,7 @@
|
||||
"roleMappingAssignRoles": "Assign Roles",
|
||||
"roleMappingAddMappingRule": "Add Mapping Rule",
|
||||
"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)",
|
||||
"roleMappingAssignRolesPlaceholderFreeform": "Type role names (exact per org)",
|
||||
"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
|
||||
PasswordExpirationPolicies = "passwordExpirationPolicies", // handle downgrade by setting to default duration
|
||||
AutoProvisioning = "autoProvisioning", // handle downgrade by disabling auto provisioning
|
||||
SshPam = "sshPam"
|
||||
SshPam = "sshPam",
|
||||
FullRbac = "fullRbac"
|
||||
}
|
||||
|
||||
export const tierMatrix: Record<TierFeature, Tier[]> = {
|
||||
@@ -48,5 +49,6 @@ export const tierMatrix: Record<TierFeature, Tier[]> = {
|
||||
"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,
|
||||
resources,
|
||||
roles,
|
||||
siteResources
|
||||
siteResources,
|
||||
userOrgRoles
|
||||
} from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
|
||||
/**
|
||||
* Get the maximum allowed retention days for a given tier
|
||||
@@ -291,6 +292,10 @@ async function disableFeature(
|
||||
await disableSshPam(orgId);
|
||||
break;
|
||||
|
||||
case TierFeature.FullRbac:
|
||||
await disableFullRbac(orgId);
|
||||
break;
|
||||
|
||||
default:
|
||||
logger.warn(
|
||||
`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> {
|
||||
const [existingBranding] = await db
|
||||
.select()
|
||||
|
||||
@@ -36,6 +36,7 @@ import { usageService } from "@server/lib/billing/usageService";
|
||||
import { build } from "@server/build";
|
||||
import { calculateUserClientsForOrgs } from "@server/lib/calculateUserClientsForOrgs";
|
||||
import { isSubscribed } from "#dynamic/lib/isSubscribed";
|
||||
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
|
||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
import {
|
||||
assignUserToOrg,
|
||||
@@ -415,7 +416,15 @@ export async function validateOidcCallback(
|
||||
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", {
|
||||
roleMappingResult
|
||||
});
|
||||
@@ -428,14 +437,14 @@ export async function validateOidcCallback(
|
||||
.where(
|
||||
and(
|
||||
eq(roles.orgId, org.orgId),
|
||||
inArray(roles.name, roleNames)
|
||||
inArray(roles.name, effectiveRoleNames)
|
||||
)
|
||||
);
|
||||
|
||||
if (!roleRes.length) {
|
||||
logger.error("No mapped roles found in organization", {
|
||||
orgId: org.orgId,
|
||||
roleNames
|
||||
roleNames: effectiveRoleNames
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -69,10 +69,7 @@ export default function AccessControlsPage() {
|
||||
const t = useTranslations();
|
||||
const { isPaidUser, hasSaasSubscription, hasEnterpriseLicense } =
|
||||
usePaidStatus();
|
||||
const multiRoleFeatureTiers = Array.from(
|
||||
new Set([...tierMatrix.sshPam, ...tierMatrix.orgOidc])
|
||||
);
|
||||
const isPaid = isPaidUser(multiRoleFeatureTiers);
|
||||
const isPaid = isPaidUser(tierMatrix.fullRbac);
|
||||
const supportsMultipleRolesPerUser = isPaid;
|
||||
const showMultiRolePaywallMessage =
|
||||
!env.flags.disableEnterpriseFeatures &&
|
||||
|
||||
@@ -5,13 +5,17 @@ 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 { useEffect, useMemo, useState } from "react";
|
||||
import { Tag, TagInput } from "@app/components/tags/tag-input";
|
||||
import {
|
||||
createMappingBuilderRule,
|
||||
MappingBuilderRule,
|
||||
RoleMappingMode
|
||||
} 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 = {
|
||||
roleId: number;
|
||||
@@ -52,10 +56,17 @@ export default function RoleMappingConfigFields({
|
||||
showFreeformRoleNamesHint = false
|
||||
}: RoleMappingConfigFieldsProps) {
|
||||
const t = useTranslations();
|
||||
const { env } = useEnvContext();
|
||||
const { isPaidUser } = usePaidStatus();
|
||||
const [activeFixedRoleTagIndex, setActiveFixedRoleTagIndex] = useState<
|
||||
number | null
|
||||
>(null);
|
||||
|
||||
const supportsMultipleRolesPerUser = isPaidUser(tierMatrix.fullRbac);
|
||||
const showSingleRoleDisclaimer =
|
||||
!env.flags.disableEnterpriseFeatures &&
|
||||
!isPaidUser(tierMatrix.fullRbac);
|
||||
|
||||
const restrictToOrgRoles = roles.length > 0;
|
||||
|
||||
const roleOptions = useMemo(
|
||||
@@ -67,13 +78,40 @@ export default function RoleMappingConfigFields({
|
||||
[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 builderRadioId = `${fieldIdPrefix}-mapping-builder-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). */
|
||||
const mappingRulesGridClass =
|
||||
"md:grid md:grid-cols-[minmax(0,1fr)_minmax(0,1.75fr)_6rem] md:gap-x-3";
|
||||
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)] md:gap-x-3";
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
@@ -119,6 +157,13 @@ export default function RoleMappingConfigFields({
|
||||
</label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
{showSingleRoleDisclaimer && (
|
||||
<FormDescription className="mt-3">
|
||||
{build === "saas"
|
||||
? t("singleRolePerUserPlanNotice")
|
||||
: t("singleRolePerUserEditionNotice")}
|
||||
</FormDescription>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{roleMappingMode === "fixedRoles" && (
|
||||
@@ -129,19 +174,37 @@ export default function RoleMappingConfigFields({
|
||||
text: name
|
||||
}))}
|
||||
setTags={(nextTags) => {
|
||||
const prevTags = fixedRoleNames.map((name) => ({
|
||||
id: name,
|
||||
text: name
|
||||
}));
|
||||
const next =
|
||||
typeof nextTags === "function"
|
||||
? nextTags(
|
||||
fixedRoleNames.map((name) => ({
|
||||
id: name,
|
||||
text: name
|
||||
}))
|
||||
)
|
||||
? nextTags(prevTags)
|
||||
: nextTags;
|
||||
|
||||
onFixedRoleNamesChange([
|
||||
let names = [
|
||||
...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}
|
||||
setActiveTagIndex={setActiveFixedRoleTagIndex}
|
||||
@@ -191,7 +254,9 @@ export default function RoleMappingConfigFields({
|
||||
<FormLabel className="min-w-0">
|
||||
{t("roleMappingAssignRoles")}
|
||||
</FormLabel>
|
||||
<span aria-hidden className="min-w-0" />
|
||||
{mappingBuilderShowsRemoveColumn ? (
|
||||
<span aria-hidden className="min-w-0" />
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{mappingBuilderRules.map((rule, index) => (
|
||||
@@ -204,6 +269,10 @@ export default function RoleMappingConfigFields({
|
||||
showFreeformRoleNamesHint={
|
||||
showFreeformRoleNamesHint
|
||||
}
|
||||
supportsMultipleRolesPerUser={
|
||||
supportsMultipleRolesPerUser
|
||||
}
|
||||
showRemoveButton={mappingBuilderShowsRemoveColumn}
|
||||
rule={rule}
|
||||
onChange={(nextRule) => {
|
||||
const nextRules = mappingBuilderRules.map(
|
||||
@@ -227,18 +296,20 @@ export default function RoleMappingConfigFields({
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
onMappingBuilderRulesChange([
|
||||
...mappingBuilderRules,
|
||||
createMappingBuilderRule()
|
||||
]);
|
||||
}}
|
||||
>
|
||||
{t("roleMappingAddMappingRule")}
|
||||
</Button>
|
||||
{supportsMultipleRolesPerUser ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
onMappingBuilderRulesChange([
|
||||
...mappingBuilderRules,
|
||||
createMappingBuilderRule()
|
||||
]);
|
||||
}}
|
||||
>
|
||||
{t("roleMappingAddMappingRule")}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -250,7 +321,11 @@ export default function RoleMappingConfigFields({
|
||||
placeholder={t("roleMappingExpressionPlaceholder")}
|
||||
/>
|
||||
<FormDescription>
|
||||
{t("roleMappingRawExpressionResultDescription")}
|
||||
{supportsMultipleRolesPerUser
|
||||
? t("roleMappingRawExpressionResultDescription")
|
||||
: t(
|
||||
"roleMappingRawExpressionResultDescriptionSingleRole"
|
||||
)}
|
||||
</FormDescription>
|
||||
</div>
|
||||
)}
|
||||
@@ -265,6 +340,8 @@ function BuilderRuleRow({
|
||||
showFreeformRoleNamesHint,
|
||||
fieldIdPrefix,
|
||||
mappingRulesGridClass,
|
||||
supportsMultipleRolesPerUser,
|
||||
showRemoveButton,
|
||||
onChange,
|
||||
onRemove
|
||||
}: {
|
||||
@@ -274,6 +351,8 @@ function BuilderRuleRow({
|
||||
showFreeformRoleNamesHint: boolean;
|
||||
fieldIdPrefix: string;
|
||||
mappingRulesGridClass: string;
|
||||
supportsMultipleRolesPerUser: boolean;
|
||||
showRemoveButton: boolean;
|
||||
onChange: (rule: MappingBuilderRule) => void;
|
||||
onRemove: () => void;
|
||||
}) {
|
||||
@@ -311,20 +390,44 @@ function BuilderRuleRow({
|
||||
text: name
|
||||
}))}
|
||||
setTags={(nextTags) => {
|
||||
const prevRoleTags = rule.roleNames.map(
|
||||
(name) => ({
|
||||
id: name,
|
||||
text: name
|
||||
})
|
||||
);
|
||||
const next =
|
||||
typeof nextTags === "function"
|
||||
? nextTags(
|
||||
rule.roleNames.map((name) => ({
|
||||
id: name,
|
||||
text: name
|
||||
}))
|
||||
)
|
||||
? nextTags(prevRoleTags)
|
||||
: 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({
|
||||
...rule,
|
||||
roleNames: [
|
||||
...new Set(next.map((tag) => tag.text))
|
||||
]
|
||||
roleNames: names
|
||||
});
|
||||
}}
|
||||
activeTagIndex={activeTagIndex}
|
||||
@@ -351,16 +454,18 @@ function BuilderRuleRow({
|
||||
</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>
|
||||
{showRemoveButton ? (
|
||||
<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>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user