Merge pull request #3193 from Infisical/misc/privilege-management-v2-transition

misc: privilege management v2 transition
This commit is contained in:
Maidul Islam
2025-03-27 01:43:31 -04:00
committed by GitHub
76 changed files with 3002 additions and 802 deletions
@@ -0,0 +1,30 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
if (!(await knex.schema.hasColumn(TableName.Organization, "shouldUseNewPrivilegeSystem"))) {
await knex.schema.alterTable(TableName.Organization, (t) => {
t.boolean("shouldUseNewPrivilegeSystem");
t.string("privilegeUpgradeInitiatedByUsername");
t.dateTime("privilegeUpgradeInitiatedAt");
});
await knex(TableName.Organization).update({
shouldUseNewPrivilegeSystem: false
});
await knex.schema.alterTable(TableName.Organization, (t) => {
t.boolean("shouldUseNewPrivilegeSystem").defaultTo(true).notNullable().alter();
});
}
}
export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasColumn(TableName.Organization, "shouldUseNewPrivilegeSystem")) {
await knex.schema.alterTable(TableName.Organization, (t) => {
t.dropColumn("shouldUseNewPrivilegeSystem");
t.dropColumn("privilegeUpgradeInitiatedByUsername");
t.dropColumn("privilegeUpgradeInitiatedAt");
});
}
}
+3
View File
@@ -23,6 +23,9 @@ export const OrganizationsSchema = z.object({
defaultMembershipRole: z.string().default("member"), defaultMembershipRole: z.string().default("member"),
enforceMfa: z.boolean().default(false), enforceMfa: z.boolean().default(false),
selectedMfaMethod: z.string().nullable().optional(), selectedMfaMethod: z.string().nullable().optional(),
shouldUseNewPrivilegeSystem: z.boolean().default(true),
privilegeUpgradeInitiatedByUsername: z.string().nullable().optional(),
privilegeUpgradeInitiatedAt: z.date().nullable().optional(),
allowSecretSharingOutsideOrganization: z.boolean().default(true).nullable().optional() allowSecretSharingOutsideOrganization: z.boolean().default(true).nullable().optional()
}); });
+77 -33
View File
@@ -3,8 +3,7 @@ import slugify from "@sindresorhus/slugify";
import { OrgMembershipRole, TOrgRoles } from "@app/db/schemas"; import { OrgMembershipRole, TOrgRoles } from "@app/db/schemas";
import { TOidcConfigDALFactory } from "@app/ee/services/oidc/oidc-config-dal"; import { TOidcConfigDALFactory } from "@app/ee/services/oidc/oidc-config-dal";
import { validatePermissionBoundary } from "@app/lib/casl/boundary"; import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors";
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid"; import { alphaNumericNanoId } from "@app/lib/nanoid";
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal"; import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
import { TOrgDALFactory } from "@app/services/org/org-dal"; import { TOrgDALFactory } from "@app/services/org/org-dal";
@@ -14,7 +13,8 @@ import { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal
import { TUserDALFactory } from "@app/services/user/user-dal"; import { TUserDALFactory } from "@app/services/user/user-dal";
import { TLicenseServiceFactory } from "../license/license-service"; import { TLicenseServiceFactory } from "../license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission"; import { OrgPermissionGroupActions, OrgPermissionSubjects } from "../permission/org-permission";
import { constructPermissionErrorMessage, validatePrivilegeChangeOperation } from "../permission/permission-fns";
import { TPermissionServiceFactory } from "../permission/permission-service"; import { TPermissionServiceFactory } from "../permission/permission-service";
import { TGroupDALFactory } from "./group-dal"; import { TGroupDALFactory } from "./group-dal";
import { addUsersToGroupByUserIds, removeUsersFromGroupByUserIds } from "./group-fns"; import { addUsersToGroupByUserIds, removeUsersFromGroupByUserIds } from "./group-fns";
@@ -67,14 +67,14 @@ export const groupServiceFactory = ({
const createGroup = async ({ name, slug, role, actor, actorId, actorAuthMethod, actorOrgId }: TCreateGroupDTO) => { const createGroup = async ({ name, slug, role, actor, actorId, actorAuthMethod, actorOrgId }: TCreateGroupDTO) => {
if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" }); if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" });
const { permission } = await permissionService.getOrgPermission( const { permission, membership } = await permissionService.getOrgPermission(
actor, actor,
actorId, actorId,
actorOrgId, actorOrgId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Groups); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Create, OrgPermissionSubjects.Groups);
const plan = await licenseService.getPlan(actorOrgId); const plan = await licenseService.getPlan(actorOrgId);
if (!plan.groups) if (!plan.groups)
@@ -87,14 +87,26 @@ export const groupServiceFactory = ({
actorOrgId actorOrgId
); );
const isCustomRole = Boolean(customRole); const isCustomRole = Boolean(customRole);
if (role !== OrgMembershipRole.NoAccess) {
const permissionBoundary = validatePrivilegeChangeOperation(
membership.shouldUseNewPrivilegeSystem,
OrgPermissionGroupActions.GrantPrivileges,
OrgPermissionSubjects.Groups,
permission,
rolePermission
);
const permissionBoundary = validatePermissionBoundary(permission, rolePermission); if (!permissionBoundary.isValid)
if (!permissionBoundary.isValid) throw new PermissionBoundaryError({
throw new ForbiddenRequestError({ message: constructPermissionErrorMessage(
name: "PermissionBoundaryError", "Failed to create group",
message: "Failed to create a more privileged group", membership.shouldUseNewPrivilegeSystem,
details: { missingPermissions: permissionBoundary.missingPermissions } OrgPermissionGroupActions.GrantPrivileges,
}); OrgPermissionSubjects.Groups
),
details: { missingPermissions: permissionBoundary.missingPermissions }
});
}
const group = await groupDAL.transaction(async (tx) => { const group = await groupDAL.transaction(async (tx) => {
const existingGroup = await groupDAL.findOne({ orgId: actorOrgId, name }, tx); const existingGroup = await groupDAL.findOne({ orgId: actorOrgId, name }, tx);
@@ -133,14 +145,15 @@ export const groupServiceFactory = ({
}: TUpdateGroupDTO) => { }: TUpdateGroupDTO) => {
if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" }); if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" });
const { permission } = await permissionService.getOrgPermission( const { permission, membership } = await permissionService.getOrgPermission(
actor, actor,
actorId, actorId,
actorOrgId, actorOrgId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Groups);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Edit, OrgPermissionSubjects.Groups);
const plan = await licenseService.getPlan(actorOrgId); const plan = await licenseService.getPlan(actorOrgId);
if (!plan.groups) if (!plan.groups)
@@ -161,11 +174,21 @@ export const groupServiceFactory = ({
); );
const isCustomRole = Boolean(customOrgRole); const isCustomRole = Boolean(customOrgRole);
const permissionBoundary = validatePermissionBoundary(permission, rolePermission); const permissionBoundary = validatePrivilegeChangeOperation(
membership.shouldUseNewPrivilegeSystem,
OrgPermissionGroupActions.GrantPrivileges,
OrgPermissionSubjects.Groups,
permission,
rolePermission
);
if (!permissionBoundary.isValid) if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({ throw new PermissionBoundaryError({
name: "PermissionBoundaryError", message: constructPermissionErrorMessage(
message: "Failed to update a more privileged group", "Failed to update group",
membership.shouldUseNewPrivilegeSystem,
OrgPermissionGroupActions.GrantPrivileges,
OrgPermissionSubjects.Groups
),
details: { missingPermissions: permissionBoundary.missingPermissions } details: { missingPermissions: permissionBoundary.missingPermissions }
}); });
if (isCustomRole) customRole = customOrgRole; if (isCustomRole) customRole = customOrgRole;
@@ -215,7 +238,7 @@ export const groupServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Groups); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Delete, OrgPermissionSubjects.Groups);
const plan = await licenseService.getPlan(actorOrgId); const plan = await licenseService.getPlan(actorOrgId);
@@ -242,7 +265,7 @@ export const groupServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Groups); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Read, OrgPermissionSubjects.Groups);
const group = await groupDAL.findById(id); const group = await groupDAL.findById(id);
if (!group) { if (!group) {
@@ -275,7 +298,7 @@ export const groupServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Groups); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Read, OrgPermissionSubjects.Groups);
const group = await groupDAL.findOne({ const group = await groupDAL.findOne({
orgId: actorOrgId, orgId: actorOrgId,
@@ -303,14 +326,14 @@ export const groupServiceFactory = ({
const addUserToGroup = async ({ id, username, actor, actorId, actorAuthMethod, actorOrgId }: TAddUserToGroupDTO) => { const addUserToGroup = async ({ id, username, actor, actorId, actorAuthMethod, actorOrgId }: TAddUserToGroupDTO) => {
if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" }); if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" });
const { permission } = await permissionService.getOrgPermission( const { permission, membership } = await permissionService.getOrgPermission(
actor, actor,
actorId, actorId,
actorOrgId, actorOrgId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Groups); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Edit, OrgPermissionSubjects.Groups);
// check if group with slug exists // check if group with slug exists
const group = await groupDAL.findOne({ const group = await groupDAL.findOne({
@@ -338,11 +361,22 @@ export const groupServiceFactory = ({
const { permission: groupRolePermission } = await permissionService.getOrgPermissionByRole(group.role, actorOrgId); const { permission: groupRolePermission } = await permissionService.getOrgPermissionByRole(group.role, actorOrgId);
// check if user has broader or equal to privileges than group // check if user has broader or equal to privileges than group
const permissionBoundary = validatePermissionBoundary(permission, groupRolePermission); const permissionBoundary = validatePrivilegeChangeOperation(
membership.shouldUseNewPrivilegeSystem,
OrgPermissionGroupActions.AddMembers,
OrgPermissionSubjects.Groups,
permission,
groupRolePermission
);
if (!permissionBoundary.isValid) if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({ throw new PermissionBoundaryError({
name: "PermissionBoundaryError", message: constructPermissionErrorMessage(
message: "Failed to add user to more privileged group", "Failed to add user to more privileged group",
membership.shouldUseNewPrivilegeSystem,
OrgPermissionGroupActions.AddMembers,
OrgPermissionSubjects.Groups
),
details: { missingPermissions: permissionBoundary.missingPermissions } details: { missingPermissions: permissionBoundary.missingPermissions }
}); });
@@ -374,14 +408,14 @@ export const groupServiceFactory = ({
}: TRemoveUserFromGroupDTO) => { }: TRemoveUserFromGroupDTO) => {
if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" }); if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" });
const { permission } = await permissionService.getOrgPermission( const { permission, membership } = await permissionService.getOrgPermission(
actor, actor,
actorId, actorId,
actorOrgId, actorOrgId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Groups); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Edit, OrgPermissionSubjects.Groups);
// check if group with slug exists // check if group with slug exists
const group = await groupDAL.findOne({ const group = await groupDAL.findOne({
@@ -409,11 +443,21 @@ export const groupServiceFactory = ({
const { permission: groupRolePermission } = await permissionService.getOrgPermissionByRole(group.role, actorOrgId); const { permission: groupRolePermission } = await permissionService.getOrgPermissionByRole(group.role, actorOrgId);
// check if user has broader or equal to privileges than group // check if user has broader or equal to privileges than group
const permissionBoundary = validatePermissionBoundary(permission, groupRolePermission); const permissionBoundary = validatePrivilegeChangeOperation(
membership.shouldUseNewPrivilegeSystem,
OrgPermissionGroupActions.RemoveMembers,
OrgPermissionSubjects.Groups,
permission,
groupRolePermission
);
if (!permissionBoundary.isValid) if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({ throw new PermissionBoundaryError({
name: "PermissionBoundaryError", message: constructPermissionErrorMessage(
message: "Failed to delete user from more privileged group", "Failed to delete user from more privileged group",
membership.shouldUseNewPrivilegeSystem,
OrgPermissionGroupActions.RemoveMembers,
OrgPermissionSubjects.Groups
),
details: { missingPermissions: permissionBoundary.missingPermissions } details: { missingPermissions: permissionBoundary.missingPermissions }
}); });
@@ -2,8 +2,7 @@ import { ForbiddenError, subject } from "@casl/ability";
import { packRules } from "@casl/ability/extra"; import { packRules } from "@casl/ability/extra";
import { ActionProjectType, TableName } from "@app/db/schemas"; import { ActionProjectType, TableName } from "@app/db/schemas";
import { validatePermissionBoundary } from "@app/lib/casl/boundary"; import { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { ms } from "@app/lib/ms"; import { ms } from "@app/lib/ms";
import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars"; import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars";
import { unpackPermissions } from "@app/server/routes/sanitizedSchema/permission"; import { unpackPermissions } from "@app/server/routes/sanitizedSchema/permission";
@@ -11,8 +10,9 @@ import { ActorType } from "@app/services/auth/auth-type";
import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal"; import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
import { constructPermissionErrorMessage, validatePrivilegeChangeOperation } from "../permission/permission-fns";
import { TPermissionServiceFactory } from "../permission/permission-service"; import { TPermissionServiceFactory } from "../permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission"; import { ProjectPermissionIdentityActions, ProjectPermissionSub } from "../permission/project-permission";
import { TIdentityProjectAdditionalPrivilegeV2DALFactory } from "./identity-project-additional-privilege-v2-dal"; import { TIdentityProjectAdditionalPrivilegeV2DALFactory } from "./identity-project-additional-privilege-v2-dal";
import { import {
IdentityProjectAdditionalPrivilegeTemporaryMode, IdentityProjectAdditionalPrivilegeTemporaryMode,
@@ -65,10 +65,10 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
actionProjectType: ActionProjectType.Any actionProjectType: ActionProjectType.Any
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit, ProjectPermissionIdentityActions.Edit,
subject(ProjectPermissionSub.Identity, { identityId }) subject(ProjectPermissionSub.Identity, { identityId })
); );
const { permission: targetIdentityPermission } = await permissionService.getProjectPermission({ const { permission: targetIdentityPermission, membership } = await permissionService.getProjectPermission({
actor: ActorType.IDENTITY, actor: ActorType.IDENTITY,
actorId: identityId, actorId: identityId,
projectId: identityProjectMembership.projectId, projectId: identityProjectMembership.projectId,
@@ -80,11 +80,21 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
// we need to validate that the privilege given is not higher than the assigning users permission // we need to validate that the privilege given is not higher than the assigning users permission
// @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules // @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules
targetIdentityPermission.update(targetIdentityPermission.rules.concat(customPermission)); targetIdentityPermission.update(targetIdentityPermission.rules.concat(customPermission));
const permissionBoundary = validatePermissionBoundary(permission, targetIdentityPermission); const permissionBoundary = validatePrivilegeChangeOperation(
membership.shouldUseNewPrivilegeSystem,
ProjectPermissionIdentityActions.GrantPrivileges,
ProjectPermissionSub.Identity,
permission,
targetIdentityPermission
);
if (!permissionBoundary.isValid) if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({ throw new PermissionBoundaryError({
name: "PermissionBoundaryError", message: constructPermissionErrorMessage(
message: "Failed to update more privileged identity", "Failed to update more privileged identity",
membership.shouldUseNewPrivilegeSystem,
ProjectPermissionIdentityActions.GrantPrivileges,
ProjectPermissionSub.Identity
),
details: { missingPermissions: permissionBoundary.missingPermissions } details: { missingPermissions: permissionBoundary.missingPermissions }
}); });
validateHandlebarTemplate("Identity Additional Privilege Create", JSON.stringify(customPermission || []), { validateHandlebarTemplate("Identity Additional Privilege Create", JSON.stringify(customPermission || []), {
@@ -154,10 +164,10 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
actionProjectType: ActionProjectType.Any actionProjectType: ActionProjectType.Any
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit, ProjectPermissionIdentityActions.Edit,
subject(ProjectPermissionSub.Identity, { identityId: identityProjectMembership.identityId }) subject(ProjectPermissionSub.Identity, { identityId: identityProjectMembership.identityId })
); );
const { permission: targetIdentityPermission } = await permissionService.getProjectPermission({ const { permission: targetIdentityPermission, membership } = await permissionService.getProjectPermission({
actor: ActorType.IDENTITY, actor: ActorType.IDENTITY,
actorId: identityProjectMembership.identityId, actorId: identityProjectMembership.identityId,
projectId: identityProjectMembership.projectId, projectId: identityProjectMembership.projectId,
@@ -169,11 +179,21 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
// we need to validate that the privilege given is not higher than the assigning users permission // we need to validate that the privilege given is not higher than the assigning users permission
// @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules // @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules
targetIdentityPermission.update(targetIdentityPermission.rules.concat(data.permissions || [])); targetIdentityPermission.update(targetIdentityPermission.rules.concat(data.permissions || []));
const permissionBoundary = validatePermissionBoundary(permission, targetIdentityPermission); const permissionBoundary = validatePrivilegeChangeOperation(
membership.shouldUseNewPrivilegeSystem,
ProjectPermissionIdentityActions.GrantPrivileges,
ProjectPermissionSub.Identity,
permission,
targetIdentityPermission
);
if (!permissionBoundary.isValid) if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({ throw new PermissionBoundaryError({
name: "PermissionBoundaryError", message: constructPermissionErrorMessage(
message: "Failed to update more privileged identity", "Failed to update more privileged identity",
membership.shouldUseNewPrivilegeSystem,
ProjectPermissionIdentityActions.GrantPrivileges,
ProjectPermissionSub.Identity
),
details: { missingPermissions: permissionBoundary.missingPermissions } details: { missingPermissions: permissionBoundary.missingPermissions }
}); });
@@ -235,7 +255,7 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
message: `Failed to find identity with membership ${identityPrivilege.projectMembershipId}` message: `Failed to find identity with membership ${identityPrivilege.projectMembershipId}`
}); });
const { permission } = await permissionService.getProjectPermission({ const { permission, membership } = await permissionService.getProjectPermission({
actor, actor,
actorId, actorId,
projectId: identityProjectMembership.projectId, projectId: identityProjectMembership.projectId,
@@ -244,7 +264,7 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
actionProjectType: ActionProjectType.Any actionProjectType: ActionProjectType.Any
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit, ProjectPermissionIdentityActions.Edit,
subject(ProjectPermissionSub.Identity, { identityId: identityProjectMembership.identityId }) subject(ProjectPermissionSub.Identity, { identityId: identityProjectMembership.identityId })
); );
const { permission: identityRolePermission } = await permissionService.getProjectPermission({ const { permission: identityRolePermission } = await permissionService.getProjectPermission({
@@ -255,11 +275,21 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
actorOrgId, actorOrgId,
actionProjectType: ActionProjectType.Any actionProjectType: ActionProjectType.Any
}); });
const permissionBoundary = validatePermissionBoundary(permission, identityRolePermission); const permissionBoundary = validatePrivilegeChangeOperation(
membership.shouldUseNewPrivilegeSystem,
ProjectPermissionIdentityActions.GrantPrivileges,
ProjectPermissionSub.Identity,
permission,
identityRolePermission
);
if (!permissionBoundary.isValid) if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({ throw new PermissionBoundaryError({
name: "PermissionBoundaryError", message: constructPermissionErrorMessage(
message: "Failed to update more privileged identity", "Failed to update more privileged identity",
membership.shouldUseNewPrivilegeSystem,
ProjectPermissionIdentityActions.GrantPrivileges,
ProjectPermissionSub.Identity
),
details: { missingPermissions: permissionBoundary.missingPermissions } details: { missingPermissions: permissionBoundary.missingPermissions }
}); });
@@ -295,7 +325,7 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
actionProjectType: ActionProjectType.Any actionProjectType: ActionProjectType.Any
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read, ProjectPermissionIdentityActions.Read,
subject(ProjectPermissionSub.Identity, { identityId: identityProjectMembership.identityId }) subject(ProjectPermissionSub.Identity, { identityId: identityProjectMembership.identityId })
); );
@@ -330,7 +360,7 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
actionProjectType: ActionProjectType.Any actionProjectType: ActionProjectType.Any
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read, ProjectPermissionIdentityActions.Read,
subject(ProjectPermissionSub.Identity, { identityId: identityProjectMembership.identityId }) subject(ProjectPermissionSub.Identity, { identityId: identityProjectMembership.identityId })
); );
@@ -366,7 +396,7 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
actionProjectType: ActionProjectType.Any actionProjectType: ActionProjectType.Any
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read, ProjectPermissionIdentityActions.Read,
subject(ProjectPermissionSub.Identity, { identityId: identityProjectMembership.identityId }) subject(ProjectPermissionSub.Identity, { identityId: identityProjectMembership.identityId })
); );
@@ -2,8 +2,7 @@ import { ForbiddenError, MongoAbility, RawRuleOf, subject } from "@casl/ability"
import { PackRule, packRules, unpackRules } from "@casl/ability/extra"; import { PackRule, packRules, unpackRules } from "@casl/ability/extra";
import { ActionProjectType } from "@app/db/schemas"; import { ActionProjectType } from "@app/db/schemas";
import { validatePermissionBoundary } from "@app/lib/casl/boundary"; import { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { ms } from "@app/lib/ms"; import { ms } from "@app/lib/ms";
import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars"; import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars";
import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission"; import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission";
@@ -11,8 +10,13 @@ import { ActorType } from "@app/services/auth/auth-type";
import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal"; import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
import { constructPermissionErrorMessage, validatePrivilegeChangeOperation } from "../permission/permission-fns";
import { TPermissionServiceFactory } from "../permission/permission-service"; import { TPermissionServiceFactory } from "../permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSet, ProjectPermissionSub } from "../permission/project-permission"; import {
ProjectPermissionIdentityActions,
ProjectPermissionSet,
ProjectPermissionSub
} from "../permission/project-permission";
import { TIdentityProjectAdditionalPrivilegeDALFactory } from "./identity-project-additional-privilege-dal"; import { TIdentityProjectAdditionalPrivilegeDALFactory } from "./identity-project-additional-privilege-dal";
import { import {
IdentityProjectAdditionalPrivilegeTemporaryMode, IdentityProjectAdditionalPrivilegeTemporaryMode,
@@ -64,7 +68,7 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
if (!identityProjectMembership) if (!identityProjectMembership)
throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` }); throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` });
const { permission } = await permissionService.getProjectPermission({ const { permission, membership } = await permissionService.getProjectPermission({
actor, actor,
actorId, actorId,
projectId: identityProjectMembership.projectId, projectId: identityProjectMembership.projectId,
@@ -72,8 +76,9 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
actorOrgId, actorOrgId,
actionProjectType: ActionProjectType.Any actionProjectType: ActionProjectType.Any
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit, ProjectPermissionIdentityActions.Edit,
subject(ProjectPermissionSub.Identity, { identityId }) subject(ProjectPermissionSub.Identity, { identityId })
); );
@@ -89,11 +94,21 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
// we need to validate that the privilege given is not higher than the assigning users permission // we need to validate that the privilege given is not higher than the assigning users permission
// @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules // @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules
targetIdentityPermission.update(targetIdentityPermission.rules.concat(customPermission)); targetIdentityPermission.update(targetIdentityPermission.rules.concat(customPermission));
const permissionBoundary = validatePermissionBoundary(permission, targetIdentityPermission); const permissionBoundary = validatePrivilegeChangeOperation(
membership.shouldUseNewPrivilegeSystem,
ProjectPermissionIdentityActions.GrantPrivileges,
ProjectPermissionSub.Identity,
permission,
targetIdentityPermission
);
if (!permissionBoundary.isValid) if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({ throw new PermissionBoundaryError({
name: "PermissionBoundaryError", message: constructPermissionErrorMessage(
message: "Failed to update more privileged identity", "Failed to update more privileged identity",
membership.shouldUseNewPrivilegeSystem,
ProjectPermissionIdentityActions.GrantPrivileges,
ProjectPermissionSub.Identity
),
details: { missingPermissions: permissionBoundary.missingPermissions } details: { missingPermissions: permissionBoundary.missingPermissions }
}); });
@@ -155,7 +170,7 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
if (!identityProjectMembership) if (!identityProjectMembership)
throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` }); throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` });
const { permission } = await permissionService.getProjectPermission({ const { permission, membership } = await permissionService.getProjectPermission({
actor, actor,
actorId, actorId,
projectId: identityProjectMembership.projectId, projectId: identityProjectMembership.projectId,
@@ -165,7 +180,7 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit, ProjectPermissionIdentityActions.Edit,
subject(ProjectPermissionSub.Identity, { identityId }) subject(ProjectPermissionSub.Identity, { identityId })
); );
@@ -181,11 +196,21 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
// we need to validate that the privilege given is not higher than the assigning users permission // we need to validate that the privilege given is not higher than the assigning users permission
// @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules // @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules
targetIdentityPermission.update(targetIdentityPermission.rules.concat(data.permissions || [])); targetIdentityPermission.update(targetIdentityPermission.rules.concat(data.permissions || []));
const permissionBoundary = validatePermissionBoundary(permission, targetIdentityPermission); const permissionBoundary = validatePrivilegeChangeOperation(
membership.shouldUseNewPrivilegeSystem,
ProjectPermissionIdentityActions.GrantPrivileges,
ProjectPermissionSub.Identity,
permission,
targetIdentityPermission
);
if (!permissionBoundary.isValid) if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({ throw new PermissionBoundaryError({
name: "PermissionBoundaryError", message: constructPermissionErrorMessage(
message: "Failed to update more privileged identity", "Failed to update more privileged identity",
membership.shouldUseNewPrivilegeSystem,
ProjectPermissionIdentityActions.GrantPrivileges,
ProjectPermissionSub.Identity
),
details: { missingPermissions: permissionBoundary.missingPermissions } details: { missingPermissions: permissionBoundary.missingPermissions }
}); });
@@ -263,7 +288,7 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
if (!identityProjectMembership) if (!identityProjectMembership)
throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` }); throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` });
const { permission } = await permissionService.getProjectPermission({ const { permission, membership } = await permissionService.getProjectPermission({
actor, actor,
actorId, actorId,
projectId: identityProjectMembership.projectId, projectId: identityProjectMembership.projectId,
@@ -272,7 +297,7 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
actionProjectType: ActionProjectType.Any actionProjectType: ActionProjectType.Any
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit, ProjectPermissionIdentityActions.Edit,
subject(ProjectPermissionSub.Identity, { identityId }) subject(ProjectPermissionSub.Identity, { identityId })
); );
@@ -284,11 +309,21 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
actorOrgId, actorOrgId,
actionProjectType: ActionProjectType.Any actionProjectType: ActionProjectType.Any
}); });
const permissionBoundary = validatePermissionBoundary(permission, identityRolePermission); const permissionBoundary = validatePrivilegeChangeOperation(
membership.shouldUseNewPrivilegeSystem,
ProjectPermissionIdentityActions.GrantPrivileges,
ProjectPermissionSub.Identity,
permission,
identityRolePermission
);
if (!permissionBoundary.isValid) if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({ throw new PermissionBoundaryError({
name: "PermissionBoundaryError", message: constructPermissionErrorMessage(
message: "Failed to edit more privileged identity", "Failed to edit more privileged identity",
membership.shouldUseNewPrivilegeSystem,
ProjectPermissionIdentityActions.GrantPrivileges,
ProjectPermissionSub.Identity
),
details: { missingPermissions: permissionBoundary.missingPermissions } details: { missingPermissions: permissionBoundary.missingPermissions }
}); });
@@ -335,7 +370,7 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
actionProjectType: ActionProjectType.Any actionProjectType: ActionProjectType.Any
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read, ProjectPermissionIdentityActions.Read,
subject(ProjectPermissionSub.Identity, { identityId }) subject(ProjectPermissionSub.Identity, { identityId })
); );
@@ -379,7 +414,7 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read, ProjectPermissionIdentityActions.Read,
subject(ProjectPermissionSub.Identity, { identityId }) subject(ProjectPermissionSub.Identity, { identityId })
); );
@@ -44,6 +44,28 @@ export enum OrgPermissionGatewayActions {
DeleteGateways = "delete-gateways" DeleteGateways = "delete-gateways"
} }
export enum OrgPermissionIdentityActions {
Read = "read",
Create = "create",
Edit = "edit",
Delete = "delete",
GrantPrivileges = "grant-privileges",
RevokeAuth = "revoke-auth",
CreateToken = "create-token",
GetToken = "get-token",
DeleteToken = "delete-token"
}
export enum OrgPermissionGroupActions {
Read = "read",
Create = "create",
Edit = "edit",
Delete = "delete",
GrantPrivileges = "grant-privileges",
AddMembers = "add-members",
RemoveMembers = "remove-members"
}
export enum OrgPermissionSubjects { export enum OrgPermissionSubjects {
Workspace = "workspace", Workspace = "workspace",
Role = "role", Role = "role",
@@ -80,10 +102,10 @@ export type OrgPermissionSet =
| [OrgPermissionActions, OrgPermissionSubjects.Sso] | [OrgPermissionActions, OrgPermissionSubjects.Sso]
| [OrgPermissionActions, OrgPermissionSubjects.Scim] | [OrgPermissionActions, OrgPermissionSubjects.Scim]
| [OrgPermissionActions, OrgPermissionSubjects.Ldap] | [OrgPermissionActions, OrgPermissionSubjects.Ldap]
| [OrgPermissionActions, OrgPermissionSubjects.Groups] | [OrgPermissionGroupActions, OrgPermissionSubjects.Groups]
| [OrgPermissionActions, OrgPermissionSubjects.SecretScanning] | [OrgPermissionActions, OrgPermissionSubjects.SecretScanning]
| [OrgPermissionActions, OrgPermissionSubjects.Billing] | [OrgPermissionActions, OrgPermissionSubjects.Billing]
| [OrgPermissionActions, OrgPermissionSubjects.Identity] | [OrgPermissionIdentityActions, OrgPermissionSubjects.Identity]
| [OrgPermissionActions, OrgPermissionSubjects.Kms] | [OrgPermissionActions, OrgPermissionSubjects.Kms]
| [OrgPermissionActions, OrgPermissionSubjects.AuditLogs] | [OrgPermissionActions, OrgPermissionSubjects.AuditLogs]
| [OrgPermissionActions, OrgPermissionSubjects.ProjectTemplates] | [OrgPermissionActions, OrgPermissionSubjects.ProjectTemplates]
@@ -256,20 +278,28 @@ const buildAdminPermission = () => {
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Ldap); can(OrgPermissionActions.Edit, OrgPermissionSubjects.Ldap);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Ldap); can(OrgPermissionActions.Delete, OrgPermissionSubjects.Ldap);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Groups); can(OrgPermissionGroupActions.Read, OrgPermissionSubjects.Groups);
can(OrgPermissionActions.Create, OrgPermissionSubjects.Groups); can(OrgPermissionGroupActions.Create, OrgPermissionSubjects.Groups);
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Groups); can(OrgPermissionGroupActions.Edit, OrgPermissionSubjects.Groups);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Groups); can(OrgPermissionGroupActions.Delete, OrgPermissionSubjects.Groups);
can(OrgPermissionGroupActions.GrantPrivileges, OrgPermissionSubjects.Groups);
can(OrgPermissionGroupActions.AddMembers, OrgPermissionSubjects.Groups);
can(OrgPermissionGroupActions.RemoveMembers, OrgPermissionSubjects.Groups);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Billing); can(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
can(OrgPermissionActions.Create, OrgPermissionSubjects.Billing); can(OrgPermissionActions.Create, OrgPermissionSubjects.Billing);
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Billing); can(OrgPermissionActions.Edit, OrgPermissionSubjects.Billing);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Billing); can(OrgPermissionActions.Delete, OrgPermissionSubjects.Billing);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Identity); can(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity);
can(OrgPermissionActions.Create, OrgPermissionSubjects.Identity); can(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity);
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); can(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Identity); can(OrgPermissionIdentityActions.Delete, OrgPermissionSubjects.Identity);
can(OrgPermissionIdentityActions.GrantPrivileges, OrgPermissionSubjects.Identity);
can(OrgPermissionIdentityActions.RevokeAuth, OrgPermissionSubjects.Identity);
can(OrgPermissionIdentityActions.CreateToken, OrgPermissionSubjects.Identity);
can(OrgPermissionIdentityActions.GetToken, OrgPermissionSubjects.Identity);
can(OrgPermissionIdentityActions.DeleteToken, OrgPermissionSubjects.Identity);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Kms); can(OrgPermissionActions.Read, OrgPermissionSubjects.Kms);
can(OrgPermissionActions.Create, OrgPermissionSubjects.Kms); can(OrgPermissionActions.Create, OrgPermissionSubjects.Kms);
@@ -316,7 +346,7 @@ const buildMemberPermission = () => {
can(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace); can(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Member); can(OrgPermissionActions.Read, OrgPermissionSubjects.Member);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Groups); can(OrgPermissionGroupActions.Read, OrgPermissionSubjects.Groups);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Role); can(OrgPermissionActions.Read, OrgPermissionSubjects.Role);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Settings); can(OrgPermissionActions.Read, OrgPermissionSubjects.Settings);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Billing); can(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
@@ -327,10 +357,10 @@ const buildMemberPermission = () => {
can(OrgPermissionActions.Edit, OrgPermissionSubjects.SecretScanning); can(OrgPermissionActions.Edit, OrgPermissionSubjects.SecretScanning);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.SecretScanning); can(OrgPermissionActions.Delete, OrgPermissionSubjects.SecretScanning);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Identity); can(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity);
can(OrgPermissionActions.Create, OrgPermissionSubjects.Identity); can(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity);
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); can(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Identity); can(OrgPermissionIdentityActions.Delete, OrgPermissionSubjects.Identity);
can(OrgPermissionActions.Read, OrgPermissionSubjects.AuditLogs); can(OrgPermissionActions.Read, OrgPermissionSubjects.AuditLogs);
@@ -49,6 +49,7 @@ export const permissionDALFactory = (db: TDbClient) => {
.join(TableName.Organization, `${TableName.Organization}.id`, `${TableName.OrgMembership}.orgId`) .join(TableName.Organization, `${TableName.Organization}.id`, `${TableName.OrgMembership}.orgId`)
.select( .select(
selectAllTableCols(TableName.OrgMembership), selectAllTableCols(TableName.OrgMembership),
db.ref("shouldUseNewPrivilegeSystem").withSchema(TableName.Organization),
db.ref("slug").withSchema(TableName.OrgRoles).withSchema(TableName.OrgRoles).as("customRoleSlug"), db.ref("slug").withSchema(TableName.OrgRoles).withSchema(TableName.OrgRoles).as("customRoleSlug"),
db.ref("permissions").withSchema(TableName.OrgRoles), db.ref("permissions").withSchema(TableName.OrgRoles),
db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"), db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"),
@@ -70,7 +71,8 @@ export const permissionDALFactory = (db: TDbClient) => {
OrgMembershipsSchema.extend({ OrgMembershipsSchema.extend({
permissions: z.unknown(), permissions: z.unknown(),
orgAuthEnforced: z.boolean().optional().nullable(), orgAuthEnforced: z.boolean().optional().nullable(),
customRoleSlug: z.string().optional().nullable() customRoleSlug: z.string().optional().nullable(),
shouldUseNewPrivilegeSystem: z.boolean()
}).parse(el), }).parse(el),
childrenMapper: [ childrenMapper: [
{ {
@@ -118,7 +120,9 @@ export const permissionDALFactory = (db: TDbClient) => {
.select(selectAllTableCols(TableName.IdentityOrgMembership)) .select(selectAllTableCols(TableName.IdentityOrgMembership))
.select(db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced")) .select(db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"))
.select("permissions") .select("permissions")
.select(db.ref("shouldUseNewPrivilegeSystem").withSchema(TableName.Organization))
.first(); .first();
return membership; return membership;
} catch (error) { } catch (error) {
throw new DatabaseError({ error, name: "GetOrgIdentityPermission" }); throw new DatabaseError({ error, name: "GetOrgIdentityPermission" });
@@ -668,7 +672,8 @@ export const permissionDALFactory = (db: TDbClient) => {
db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"), db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"),
db.ref("orgId").withSchema(TableName.Project), db.ref("orgId").withSchema(TableName.Project),
db.ref("type").withSchema(TableName.Project).as("projectType"), db.ref("type").withSchema(TableName.Project).as("projectType"),
db.ref("id").withSchema(TableName.Project).as("projectId") db.ref("id").withSchema(TableName.Project).as("projectId"),
db.ref("shouldUseNewPrivilegeSystem").withSchema(TableName.Organization)
); );
const [userPermission] = sqlNestRelationships({ const [userPermission] = sqlNestRelationships({
@@ -684,7 +689,8 @@ export const permissionDALFactory = (db: TDbClient) => {
groupMembershipCreatedAt, groupMembershipCreatedAt,
groupMembershipUpdatedAt, groupMembershipUpdatedAt,
membershipUpdatedAt, membershipUpdatedAt,
projectType projectType,
shouldUseNewPrivilegeSystem
}) => ({ }) => ({
orgId, orgId,
orgAuthEnforced, orgAuthEnforced,
@@ -694,7 +700,8 @@ export const permissionDALFactory = (db: TDbClient) => {
projectType, projectType,
id: membershipId || groupMembershipId, id: membershipId || groupMembershipId,
createdAt: membershipCreatedAt || groupMembershipCreatedAt, createdAt: membershipCreatedAt || groupMembershipCreatedAt,
updatedAt: membershipUpdatedAt || groupMembershipUpdatedAt updatedAt: membershipUpdatedAt || groupMembershipUpdatedAt,
shouldUseNewPrivilegeSystem
}), }),
childrenMapper: [ childrenMapper: [
{ {
@@ -995,6 +1002,7 @@ export const permissionDALFactory = (db: TDbClient) => {
`${TableName.IdentityProjectMembership}.projectId`, `${TableName.IdentityProjectMembership}.projectId`,
`${TableName.Project}.id` `${TableName.Project}.id`
) )
.join(TableName.Organization, `${TableName.Project}.orgId`, `${TableName.Organization}.id`)
.leftJoin(TableName.IdentityMetadata, (queryBuilder) => { .leftJoin(TableName.IdentityMetadata, (queryBuilder) => {
void queryBuilder void queryBuilder
.on(`${TableName.Identity}.id`, `${TableName.IdentityMetadata}.identityId`) .on(`${TableName.Identity}.id`, `${TableName.IdentityMetadata}.identityId`)
@@ -1012,6 +1020,7 @@ export const permissionDALFactory = (db: TDbClient) => {
db.ref("updatedAt").withSchema(TableName.IdentityProjectMembership).as("membershipUpdatedAt"), db.ref("updatedAt").withSchema(TableName.IdentityProjectMembership).as("membershipUpdatedAt"),
db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug"), db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug"),
db.ref("permissions").withSchema(TableName.ProjectRoles), db.ref("permissions").withSchema(TableName.ProjectRoles),
db.ref("shouldUseNewPrivilegeSystem").withSchema(TableName.Organization),
db.ref("id").withSchema(TableName.IdentityProjectAdditionalPrivilege).as("identityApId"), db.ref("id").withSchema(TableName.IdentityProjectAdditionalPrivilege).as("identityApId"),
db.ref("permissions").withSchema(TableName.IdentityProjectAdditionalPrivilege).as("identityApPermissions"), db.ref("permissions").withSchema(TableName.IdentityProjectAdditionalPrivilege).as("identityApPermissions"),
db db
@@ -1045,7 +1054,8 @@ export const permissionDALFactory = (db: TDbClient) => {
membershipUpdatedAt, membershipUpdatedAt,
orgId, orgId,
identityName, identityName,
projectType projectType,
shouldUseNewPrivilegeSystem
}) => ({ }) => ({
id: membershipId, id: membershipId,
identityId, identityId,
@@ -1055,6 +1065,7 @@ export const permissionDALFactory = (db: TDbClient) => {
updatedAt: membershipUpdatedAt, updatedAt: membershipUpdatedAt,
orgId, orgId,
projectType, projectType,
shouldUseNewPrivilegeSystem,
// just a prefilled value // just a prefilled value
orgAuthEnforced: false orgAuthEnforced: false
}), }),
@@ -3,9 +3,11 @@ import { ForbiddenError, MongoAbility, PureAbility, subject } from "@casl/abilit
import { z } from "zod"; import { z } from "zod";
import { TOrganizations } from "@app/db/schemas"; import { TOrganizations } from "@app/db/schemas";
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
import { BadRequestError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors"; import { BadRequestError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors";
import { ActorAuthMethod, AuthMethod } from "@app/services/auth/auth-type"; import { ActorAuthMethod, AuthMethod } from "@app/services/auth/auth-type";
import { OrgPermissionSet } from "./org-permission";
import { import {
ProjectPermissionSecretActions, ProjectPermissionSecretActions,
ProjectPermissionSet, ProjectPermissionSet,
@@ -145,4 +147,57 @@ const escapeHandlebarsMissingDict = (obj: Record<string, string>, key: string) =
return new Proxy(obj, handler); return new Proxy(obj, handler);
}; };
export { escapeHandlebarsMissingDict, isAuthMethodSaml, validateOrgSSO }; // This function serves as a transition layer between the old and new privilege management system
// the old privilege management system is based on the actor having more privileges than the managed permission
// the new privilege management system is based on the actor having the appropriate permission to perform the privilege change,
// regardless of the actor's privilege level.
const validatePrivilegeChangeOperation = (
shouldUseNewPrivilegeSystem: boolean,
opAction: OrgPermissionSet[0] | ProjectPermissionSet[0],
opSubject: OrgPermissionSet[1] | ProjectPermissionSet[1],
actorPermission: MongoAbility,
managedPermission: MongoAbility
) => {
if (shouldUseNewPrivilegeSystem) {
if (actorPermission.can(opAction, opSubject)) {
return {
isValid: true,
missingPermissions: []
};
}
return {
isValid: false,
missingPermissions: [
{
action: opAction,
subject: opSubject
}
]
};
}
// if not, we check if the actor is indeed more privileged than the managed permission - this is the old system
return validatePermissionBoundary(actorPermission, managedPermission);
};
const constructPermissionErrorMessage = (
baseMessage: string,
shouldUseNewPrivilegeSystem: boolean,
opAction: OrgPermissionSet[0] | ProjectPermissionSet[0],
opSubject: OrgPermissionSet[1] | ProjectPermissionSet[1]
) => {
return `${baseMessage}${
shouldUseNewPrivilegeSystem
? `. Actor is missing permission ${opAction as string} on ${opSubject as string}`
: ". Actor privilege level is not high enough to perform this action"
}`;
};
export {
constructPermissionErrorMessage,
escapeHandlebarsMissingDict,
isAuthMethodSaml,
validateOrgSSO,
validatePrivilegeChangeOperation
};
@@ -397,14 +397,18 @@ export const permissionServiceFactory = ({
const scopes = ServiceTokenScopes.parse(serviceToken.scopes || []); const scopes = ServiceTokenScopes.parse(serviceToken.scopes || []);
return { return {
permission: buildServiceTokenProjectPermission(scopes, serviceToken.permissions), permission: buildServiceTokenProjectPermission(scopes, serviceToken.permissions),
membership: undefined membership: {
shouldUseNewPrivilegeSystem: true
}
}; };
}; };
type TProjectPermissionRT<T extends ActorType> = T extends ActorType.SERVICE type TProjectPermissionRT<T extends ActorType> = T extends ActorType.SERVICE
? { ? {
permission: MongoAbility<ProjectPermissionSet, MongoQuery>; permission: MongoAbility<ProjectPermissionSet, MongoQuery>;
membership: undefined; membership: {
shouldUseNewPrivilegeSystem: boolean;
};
hasRole: (arg: string) => boolean; hasRole: (arg: string) => boolean;
} // service token doesn't have both membership and roles } // service token doesn't have both membership and roles
: { : {
@@ -413,6 +417,7 @@ export const permissionServiceFactory = ({
orgAuthEnforced: boolean | null | undefined; orgAuthEnforced: boolean | null | undefined;
orgId: string; orgId: string;
roles: Array<{ role: string }>; roles: Array<{ role: string }>;
shouldUseNewPrivilegeSystem: boolean;
}; };
hasRole: (role: string) => boolean; hasRole: (role: string) => boolean;
}; };
@@ -43,6 +43,30 @@ export enum ProjectPermissionDynamicSecretActions {
Lease = "lease" Lease = "lease"
} }
export enum ProjectPermissionIdentityActions {
Read = "read",
Create = "create",
Edit = "edit",
Delete = "delete",
GrantPrivileges = "grant-privileges"
}
export enum ProjectPermissionMemberActions {
Read = "read",
Create = "create",
Edit = "edit",
Delete = "delete",
GrantPrivileges = "grant-privileges"
}
export enum ProjectPermissionGroupActions {
Read = "read",
Create = "create",
Edit = "edit",
Delete = "delete",
GrantPrivileges = "grant-privileges"
}
export enum ProjectPermissionSecretSyncActions { export enum ProjectPermissionSecretSyncActions {
Read = "read", Read = "read",
Create = "create", Create = "create",
@@ -150,8 +174,8 @@ export type ProjectPermissionSet =
] ]
| [ProjectPermissionActions, ProjectPermissionSub.Role] | [ProjectPermissionActions, ProjectPermissionSub.Role]
| [ProjectPermissionActions, ProjectPermissionSub.Tags] | [ProjectPermissionActions, ProjectPermissionSub.Tags]
| [ProjectPermissionActions, ProjectPermissionSub.Member] | [ProjectPermissionMemberActions, ProjectPermissionSub.Member]
| [ProjectPermissionActions, ProjectPermissionSub.Groups] | [ProjectPermissionGroupActions, ProjectPermissionSub.Groups]
| [ProjectPermissionActions, ProjectPermissionSub.Integrations] | [ProjectPermissionActions, ProjectPermissionSub.Integrations]
| [ProjectPermissionActions, ProjectPermissionSub.Webhooks] | [ProjectPermissionActions, ProjectPermissionSub.Webhooks]
| [ProjectPermissionActions, ProjectPermissionSub.AuditLogs] | [ProjectPermissionActions, ProjectPermissionSub.AuditLogs]
@@ -162,7 +186,7 @@ export type ProjectPermissionSet =
| [ProjectPermissionActions, ProjectPermissionSub.SecretApproval] | [ProjectPermissionActions, ProjectPermissionSub.SecretApproval]
| [ProjectPermissionActions, ProjectPermissionSub.SecretRotation] | [ProjectPermissionActions, ProjectPermissionSub.SecretRotation]
| [ | [
ProjectPermissionActions, ProjectPermissionIdentityActions,
ProjectPermissionSub.Identity | (ForcedSubject<ProjectPermissionSub.Identity> & IdentityManagementSubjectFields) ProjectPermissionSub.Identity | (ForcedSubject<ProjectPermissionSub.Identity> & IdentityManagementSubjectFields)
] ]
| [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities] | [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities]
@@ -290,13 +314,13 @@ const GeneralPermissionSchema = [
}), }),
z.object({ z.object({
subject: z.literal(ProjectPermissionSub.Member).describe("The entity this permission pertains to."), subject: z.literal(ProjectPermissionSub.Member).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe( action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionMemberActions).describe(
"Describe what action an entity can take." "Describe what action an entity can take."
) )
}), }),
z.object({ z.object({
subject: z.literal(ProjectPermissionSub.Groups).describe("The entity this permission pertains to."), subject: z.literal(ProjectPermissionSub.Groups).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe( action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionGroupActions).describe(
"Describe what action an entity can take." "Describe what action an entity can take."
) )
}), }),
@@ -510,7 +534,7 @@ export const ProjectPermissionV2Schema = z.discriminatedUnion("subject", [
z.object({ z.object({
subject: z.literal(ProjectPermissionSub.Identity).describe("The entity this permission pertains to."), subject: z.literal(ProjectPermissionSub.Identity).describe("The entity this permission pertains to."),
inverted: z.boolean().optional().describe("Whether rule allows or forbids."), inverted: z.boolean().optional().describe("Whether rule allows or forbids."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe( action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionIdentityActions).describe(
"Describe what action an entity can take." "Describe what action an entity can take."
), ),
conditions: IdentityManagementConditionSchema.describe( conditions: IdentityManagementConditionSchema.describe(
@@ -531,12 +555,9 @@ const buildAdminPermissionRules = () => {
ProjectPermissionSub.SecretImports, ProjectPermissionSub.SecretImports,
ProjectPermissionSub.SecretApproval, ProjectPermissionSub.SecretApproval,
ProjectPermissionSub.SecretRotation, ProjectPermissionSub.SecretRotation,
ProjectPermissionSub.Member,
ProjectPermissionSub.Groups,
ProjectPermissionSub.Role, ProjectPermissionSub.Role,
ProjectPermissionSub.Integrations, ProjectPermissionSub.Integrations,
ProjectPermissionSub.Webhooks, ProjectPermissionSub.Webhooks,
ProjectPermissionSub.Identity,
ProjectPermissionSub.ServiceTokens, ProjectPermissionSub.ServiceTokens,
ProjectPermissionSub.Settings, ProjectPermissionSub.Settings,
ProjectPermissionSub.Environments, ProjectPermissionSub.Environments,
@@ -563,6 +584,39 @@ const buildAdminPermissionRules = () => {
); );
}); });
can(
[
ProjectPermissionMemberActions.Create,
ProjectPermissionMemberActions.Edit,
ProjectPermissionMemberActions.Delete,
ProjectPermissionMemberActions.Read,
ProjectPermissionMemberActions.GrantPrivileges
],
ProjectPermissionSub.Member
);
can(
[
ProjectPermissionGroupActions.Create,
ProjectPermissionGroupActions.Edit,
ProjectPermissionGroupActions.Delete,
ProjectPermissionGroupActions.Read,
ProjectPermissionGroupActions.GrantPrivileges
],
ProjectPermissionSub.Groups
);
can(
[
ProjectPermissionIdentityActions.Create,
ProjectPermissionIdentityActions.Edit,
ProjectPermissionIdentityActions.Delete,
ProjectPermissionIdentityActions.Read,
ProjectPermissionIdentityActions.GrantPrivileges
],
ProjectPermissionSub.Identity
);
can( can(
[ [
ProjectPermissionSecretActions.DescribeAndReadValue, ProjectPermissionSecretActions.DescribeAndReadValue,
@@ -677,9 +731,9 @@ const buildMemberPermissionRules = () => {
can([ProjectPermissionActions.Read, ProjectPermissionActions.Create], ProjectPermissionSub.SecretRollback); can([ProjectPermissionActions.Read, ProjectPermissionActions.Create], ProjectPermissionSub.SecretRollback);
can([ProjectPermissionActions.Read, ProjectPermissionActions.Create], ProjectPermissionSub.Member); can([ProjectPermissionMemberActions.Read, ProjectPermissionMemberActions.Create], ProjectPermissionSub.Member);
can([ProjectPermissionActions.Read], ProjectPermissionSub.Groups); can([ProjectPermissionGroupActions.Read], ProjectPermissionSub.Groups);
can( can(
[ [
@@ -703,10 +757,10 @@ const buildMemberPermissionRules = () => {
can( can(
[ [
ProjectPermissionActions.Read, ProjectPermissionIdentityActions.Read,
ProjectPermissionActions.Edit, ProjectPermissionIdentityActions.Edit,
ProjectPermissionActions.Create, ProjectPermissionIdentityActions.Create,
ProjectPermissionActions.Delete ProjectPermissionIdentityActions.Delete
], ],
ProjectPermissionSub.Identity ProjectPermissionSub.Identity
); );
@@ -820,12 +874,12 @@ const buildViewerPermissionRules = () => {
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval); can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback); can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation); can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Member); can(ProjectPermissionMemberActions.Read, ProjectPermissionSub.Member);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Groups); can(ProjectPermissionGroupActions.Read, ProjectPermissionSub.Groups);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Role); can(ProjectPermissionActions.Read, ProjectPermissionSub.Role);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks); can(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Identity); can(ProjectPermissionIdentityActions.Read, ProjectPermissionSub.Identity);
can(ProjectPermissionActions.Read, ProjectPermissionSub.ServiceTokens); can(ProjectPermissionActions.Read, ProjectPermissionSub.ServiceTokens);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Settings); can(ProjectPermissionActions.Read, ProjectPermissionSub.Settings);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Environments); can(ProjectPermissionActions.Read, ProjectPermissionSub.Environments);
@@ -2,16 +2,20 @@ import { ForbiddenError, MongoAbility, RawRuleOf } from "@casl/ability";
import { PackRule, packRules, unpackRules } from "@casl/ability/extra"; import { PackRule, packRules, unpackRules } from "@casl/ability/extra";
import { ActionProjectType, TableName } from "@app/db/schemas"; import { ActionProjectType, TableName } from "@app/db/schemas";
import { validatePermissionBoundary } from "@app/lib/casl/boundary"; import { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { ms } from "@app/lib/ms"; import { ms } from "@app/lib/ms";
import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars"; import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars";
import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission"; import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission";
import { ActorType } from "@app/services/auth/auth-type"; import { ActorType } from "@app/services/auth/auth-type";
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal"; import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
import { constructPermissionErrorMessage, validatePrivilegeChangeOperation } from "../permission/permission-fns";
import { TPermissionServiceFactory } from "../permission/permission-service"; import { TPermissionServiceFactory } from "../permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSet, ProjectPermissionSub } from "../permission/project-permission"; import {
ProjectPermissionMemberActions,
ProjectPermissionSet,
ProjectPermissionSub
} from "../permission/project-permission";
import { TProjectUserAdditionalPrivilegeDALFactory } from "./project-user-additional-privilege-dal"; import { TProjectUserAdditionalPrivilegeDALFactory } from "./project-user-additional-privilege-dal";
import { import {
ProjectUserAdditionalPrivilegeTemporaryMode, ProjectUserAdditionalPrivilegeTemporaryMode,
@@ -64,8 +68,8 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
actorOrgId, actorOrgId,
actionProjectType: ActionProjectType.Any actionProjectType: ActionProjectType.Any
}); });
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Edit, ProjectPermissionSub.Member);
const { permission: targetUserPermission } = await permissionService.getProjectPermission({ const { permission: targetUserPermission, membership } = await permissionService.getProjectPermission({
actor: ActorType.USER, actor: ActorType.USER,
actorId: projectMembership.userId, actorId: projectMembership.userId,
projectId: projectMembership.projectId, projectId: projectMembership.projectId,
@@ -77,11 +81,21 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
// we need to validate that the privilege given is not higher than the assigning users permission // we need to validate that the privilege given is not higher than the assigning users permission
// @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules // @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules
targetUserPermission.update(targetUserPermission.rules.concat(customPermission)); targetUserPermission.update(targetUserPermission.rules.concat(customPermission));
const permissionBoundary = validatePermissionBoundary(permission, targetUserPermission); const permissionBoundary = validatePrivilegeChangeOperation(
membership.shouldUseNewPrivilegeSystem,
ProjectPermissionMemberActions.GrantPrivileges,
ProjectPermissionSub.Member,
permission,
targetUserPermission
);
if (!permissionBoundary.isValid) if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({ throw new PermissionBoundaryError({
name: "PermissionBoundaryError", message: constructPermissionErrorMessage(
message: "Failed to update more privileged user", "Failed to update more privileged user",
membership.shouldUseNewPrivilegeSystem,
ProjectPermissionMemberActions.GrantPrivileges,
ProjectPermissionSub.Member
),
details: { missingPermissions: permissionBoundary.missingPermissions } details: { missingPermissions: permissionBoundary.missingPermissions }
}); });
@@ -151,7 +165,7 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
message: `Project membership for user with ID '${userPrivilege.userId}' not found in project with ID '${userPrivilege.projectId}'` message: `Project membership for user with ID '${userPrivilege.userId}' not found in project with ID '${userPrivilege.projectId}'`
}); });
const { permission } = await permissionService.getProjectPermission({ const { permission, membership } = await permissionService.getProjectPermission({
actor, actor,
actorId, actorId,
projectId: projectMembership.projectId, projectId: projectMembership.projectId,
@@ -159,7 +173,7 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
actorOrgId, actorOrgId,
actionProjectType: ActionProjectType.Any actionProjectType: ActionProjectType.Any
}); });
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Edit, ProjectPermissionSub.Member);
const { permission: targetUserPermission } = await permissionService.getProjectPermission({ const { permission: targetUserPermission } = await permissionService.getProjectPermission({
actor: ActorType.USER, actor: ActorType.USER,
actorId: projectMembership.userId, actorId: projectMembership.userId,
@@ -172,11 +186,21 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
// we need to validate that the privilege given is not higher than the assigning users permission // we need to validate that the privilege given is not higher than the assigning users permission
// @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules // @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules
targetUserPermission.update(targetUserPermission.rules.concat(dto.permissions || [])); targetUserPermission.update(targetUserPermission.rules.concat(dto.permissions || []));
const permissionBoundary = validatePermissionBoundary(permission, targetUserPermission); const permissionBoundary = validatePrivilegeChangeOperation(
membership.shouldUseNewPrivilegeSystem,
ProjectPermissionMemberActions.GrantPrivileges,
ProjectPermissionSub.Member,
permission,
targetUserPermission
);
if (!permissionBoundary.isValid) if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({ throw new PermissionBoundaryError({
name: "PermissionBoundaryError", message: constructPermissionErrorMessage(
message: "Failed to update more privileged identity", "Failed to update more privileged user",
membership.shouldUseNewPrivilegeSystem,
ProjectPermissionMemberActions.GrantPrivileges,
ProjectPermissionSub.Member
),
details: { missingPermissions: permissionBoundary.missingPermissions } details: { missingPermissions: permissionBoundary.missingPermissions }
}); });
@@ -253,7 +277,7 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
actorOrgId, actorOrgId,
actionProjectType: ActionProjectType.Any actionProjectType: ActionProjectType.Any
}); });
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Edit, ProjectPermissionSub.Member);
const deletedPrivilege = await projectUserAdditionalPrivilegeDAL.deleteById(userPrivilege.id); const deletedPrivilege = await projectUserAdditionalPrivilegeDAL.deleteById(userPrivilege.id);
return { return {
@@ -290,7 +314,7 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
actorOrgId, actorOrgId,
actionProjectType: ActionProjectType.Any actionProjectType: ActionProjectType.Any
}); });
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Member); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Read, ProjectPermissionSub.Member);
return { return {
...userPrivilege, ...userPrivilege,
@@ -317,7 +341,7 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
actorOrgId, actorOrgId,
actionProjectType: ActionProjectType.Any actionProjectType: ActionProjectType.Any
}); });
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Member); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Read, ProjectPermissionSub.Member);
const userPrivileges = await projectUserAdditionalPrivilegeDAL.find( const userPrivileges = await projectUserAdditionalPrivilegeDAL.find(
{ {
+17
View File
@@ -68,6 +68,23 @@ export class ForbiddenRequestError extends Error {
} }
} }
export class PermissionBoundaryError extends ForbiddenRequestError {
constructor({
message,
name,
error,
details
}: {
message?: string;
name?: string;
error?: unknown;
details?: unknown;
}) {
super({ message, name, error, details });
this.name = "PermissionBoundaryError";
}
}
export class BadRequestError extends Error { export class BadRequestError extends Error {
name: string; name: string;
+2 -1
View File
@@ -13,6 +13,7 @@ import {
InternalServerError, InternalServerError,
NotFoundError, NotFoundError,
OidcAuthError, OidcAuthError,
PermissionBoundaryError,
RateLimitError, RateLimitError,
ScimRequestError, ScimRequestError,
UnauthorizedError UnauthorizedError
@@ -117,7 +118,7 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider
conditions: el.conditions conditions: el.conditions
})) }))
}); });
} else if (error instanceof ForbiddenRequestError) { } else if (error instanceof ForbiddenRequestError || error instanceof PermissionBoundaryError) {
void res.status(HttpStatusCodes.Forbidden).send({ void res.status(HttpStatusCodes.Forbidden).send({
reqId: req.id, reqId: req.id,
statusCode: HttpStatusCodes.Forbidden, statusCode: HttpStatusCodes.Forbidden,
@@ -4,7 +4,6 @@ import {
AuditLogsSchema, AuditLogsSchema,
GroupsSchema, GroupsSchema,
IncidentContactsSchema, IncidentContactsSchema,
OrganizationsSchema,
OrgMembershipsSchema, OrgMembershipsSchema,
OrgRolesSchema, OrgRolesSchema,
UsersSchema UsersSchema
@@ -57,7 +56,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
}), }),
response: { response: {
200: z.object({ 200: z.object({
organization: OrganizationsSchema organization: sanitizedOrganizationSchema
}) })
} }
}, },
@@ -263,7 +262,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
response: { response: {
200: z.object({ 200: z.object({
message: z.string(), message: z.string(),
organization: OrganizationsSchema organization: sanitizedOrganizationSchema
}) })
} }
}, },
@@ -1,7 +1,6 @@
import { z } from "zod"; import { z } from "zod";
import { import {
OrganizationsSchema,
OrgMembershipsSchema, OrgMembershipsSchema,
ProjectMembershipsSchema, ProjectMembershipsSchema,
ProjectsSchema, ProjectsSchema,
@@ -15,6 +14,7 @@ import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { GenericResourceNameSchema } from "@app/server/lib/schemas"; import { GenericResourceNameSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { ActorType, AuthMode } from "@app/services/auth/auth-type"; import { ActorType, AuthMode } from "@app/services/auth/auth-type";
import { sanitizedOrganizationSchema } from "@app/services/org/org-schema";
export const registerOrgRouter = async (server: FastifyZodProvider) => { export const registerOrgRouter = async (server: FastifyZodProvider) => {
server.route({ server.route({
@@ -335,7 +335,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
}), }),
response: { response: {
200: z.object({ 200: z.object({
organization: OrganizationsSchema organization: sanitizedOrganizationSchema
}) })
} }
}, },
@@ -365,7 +365,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
}), }),
response: { response: {
200: z.object({ 200: z.object({
organization: OrganizationsSchema, organization: sanitizedOrganizationSchema,
accessToken: z.string() accessToken: z.string()
}) })
} }
@@ -396,4 +396,30 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
return { organization, accessToken: tokens.accessToken }; return { organization, accessToken: tokens.accessToken };
} }
}); });
server.route({
method: "POST",
url: "/privilege-system-upgrade",
config: {
rateLimit: writeLimit
},
schema: {
response: {
200: z.object({
organization: sanitizedOrganizationSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const organization = await server.services.org.upgradePrivilegeSystem({
actorId: req.permission.id,
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
orgId: req.permission.orgId
});
return { organization };
}
});
}; };
@@ -1,12 +1,15 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import { ActionProjectType, ProjectMembershipRole, SecretKeyEncoding, TGroups } from "@app/db/schemas"; import { ActionProjectType, ProjectMembershipRole, SecretKeyEncoding, TGroups } from "@app/db/schemas";
import {
constructPermissionErrorMessage,
validatePrivilegeChangeOperation
} from "@app/ee/services/permission/permission-fns";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionGroupActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
import { decryptAsymmetric, encryptAsymmetric } from "@app/lib/crypto"; import { decryptAsymmetric, encryptAsymmetric } from "@app/lib/crypto";
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption"; import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors";
import { groupBy } from "@app/lib/fn"; import { groupBy } from "@app/lib/fn";
import { ms } from "@app/lib/ms"; import { ms } from "@app/lib/ms";
import { isUuidV4 } from "@app/lib/validator"; import { isUuidV4 } from "@app/lib/validator";
@@ -70,7 +73,7 @@ export const groupProjectServiceFactory = ({
if (!project) throw new NotFoundError({ message: `Failed to find project with ID ${projectId}` }); if (!project) throw new NotFoundError({ message: `Failed to find project with ID ${projectId}` });
if (project.version < 2) throw new BadRequestError({ message: `Failed to add group to E2EE project` }); if (project.version < 2) throw new BadRequestError({ message: `Failed to add group to E2EE project` });
const { permission } = await permissionService.getProjectPermission({ const { permission, membership } = await permissionService.getProjectPermission({
actor, actor,
actorId, actorId,
projectId, projectId,
@@ -78,7 +81,7 @@ export const groupProjectServiceFactory = ({
actorOrgId, actorOrgId,
actionProjectType: ActionProjectType.Any actionProjectType: ActionProjectType.Any
}); });
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Groups); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionGroupActions.Create, ProjectPermissionSub.Groups);
let group: TGroups | null = null; let group: TGroups | null = null;
if (isUuidV4(groupIdOrName)) { if (isUuidV4(groupIdOrName)) {
@@ -102,11 +105,21 @@ export const groupProjectServiceFactory = ({
project.id project.id
); );
const permissionBoundary = validatePermissionBoundary(permission, rolePermission); const permissionBoundary = validatePrivilegeChangeOperation(
membership.shouldUseNewPrivilegeSystem,
ProjectPermissionGroupActions.GrantPrivileges,
ProjectPermissionSub.Groups,
permission,
rolePermission
);
if (!permissionBoundary.isValid) if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({ throw new PermissionBoundaryError({
name: "PermissionBoundaryError", message: constructPermissionErrorMessage(
message: "Failed to assign group to a more privileged role", "Failed to assign group to role",
membership.shouldUseNewPrivilegeSystem,
ProjectPermissionGroupActions.GrantPrivileges,
ProjectPermissionSub.Groups
),
details: { missingPermissions: permissionBoundary.missingPermissions } details: { missingPermissions: permissionBoundary.missingPermissions }
}); });
} }
@@ -248,7 +261,7 @@ export const groupProjectServiceFactory = ({
if (!project) throw new NotFoundError({ message: `Failed to find project with ID ${projectId}` }); if (!project) throw new NotFoundError({ message: `Failed to find project with ID ${projectId}` });
const { permission } = await permissionService.getProjectPermission({ const { permission, membership } = await permissionService.getProjectPermission({
actor, actor,
actorId, actorId,
projectId, projectId,
@@ -256,7 +269,7 @@ export const groupProjectServiceFactory = ({
actorOrgId, actorOrgId,
actionProjectType: ActionProjectType.Any actionProjectType: ActionProjectType.Any
}); });
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Groups); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionGroupActions.Edit, ProjectPermissionSub.Groups);
const group = await groupDAL.findOne({ orgId: actorOrgId, id: groupId }); const group = await groupDAL.findOne({ orgId: actorOrgId, id: groupId });
if (!group) throw new NotFoundError({ message: `Failed to find group with ID ${groupId}` }); if (!group) throw new NotFoundError({ message: `Failed to find group with ID ${groupId}` });
@@ -269,11 +282,21 @@ export const groupProjectServiceFactory = ({
requestedRoleChange, requestedRoleChange,
project.id project.id
); );
const permissionBoundary = validatePermissionBoundary(permission, rolePermission); const permissionBoundary = validatePrivilegeChangeOperation(
membership.shouldUseNewPrivilegeSystem,
ProjectPermissionGroupActions.GrantPrivileges,
ProjectPermissionSub.Groups,
permission,
rolePermission
);
if (!permissionBoundary.isValid) if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({ throw new PermissionBoundaryError({
name: "PermissionBoundaryError", message: constructPermissionErrorMessage(
message: "Failed to assign group to a more privileged role", "Failed to assign group to role",
membership.shouldUseNewPrivilegeSystem,
ProjectPermissionGroupActions.GrantPrivileges,
ProjectPermissionSub.Groups
),
details: { missingPermissions: permissionBoundary.missingPermissions } details: { missingPermissions: permissionBoundary.missingPermissions }
}); });
} }
@@ -360,7 +383,7 @@ export const groupProjectServiceFactory = ({
actorOrgId, actorOrgId,
actionProjectType: ActionProjectType.Any actionProjectType: ActionProjectType.Any
}); });
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Groups); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionGroupActions.Delete, ProjectPermissionSub.Groups);
const deletedProjectGroup = await groupProjectDAL.transaction(async (tx) => { const deletedProjectGroup = await groupProjectDAL.transaction(async (tx) => {
const groupMembers = await userGroupMembershipDAL.findGroupMembersNotInProject(group.id, project.id, tx); const groupMembers = await userGroupMembershipDAL.findGroupMembersNotInProject(group.id, project.id, tx);
@@ -405,7 +428,7 @@ export const groupProjectServiceFactory = ({
actorOrgId, actorOrgId,
actionProjectType: ActionProjectType.Any actionProjectType: ActionProjectType.Any
}); });
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Groups); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionGroupActions.Read, ProjectPermissionSub.Groups);
const groupMemberships = await groupProjectDAL.findByProjectId(project.id); const groupMemberships = await groupProjectDAL.findByProjectId(project.id);
return groupMemberships; return groupMemberships;
@@ -433,7 +456,7 @@ export const groupProjectServiceFactory = ({
actorOrgId, actorOrgId,
actionProjectType: ActionProjectType.Any actionProjectType: ActionProjectType.Any
}); });
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Groups); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionGroupActions.Read, ProjectPermissionSub.Groups);
const [groupMembership] = await groupProjectDAL.findByProjectId(project.id, { const [groupMembership] = await groupProjectDAL.findByProjectId(project.id, {
groupId groupId
@@ -5,11 +5,14 @@ import jwt from "jsonwebtoken";
import { IdentityAuthMethod } from "@app/db/schemas"; import { IdentityAuthMethod } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import {
constructPermissionErrorMessage,
validatePrivilegeChangeOperation
} from "@app/ee/services/permission/permission-fns";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
import { ActorType, AuthTokenType } from "../auth/auth-type"; import { ActorType, AuthTokenType } from "../auth/auth-type";
@@ -176,7 +179,7 @@ export const identityAwsAuthServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Identity); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity);
const plan = await licenseService.getPlan(identityMembershipOrg.orgId); const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => { const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => {
@@ -255,7 +258,7 @@ export const identityAwsAuthServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
const plan = await licenseService.getPlan(identityMembershipOrg.orgId); const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => { const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => {
@@ -309,7 +312,7 @@ export const identityAwsAuthServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity);
return { ...awsIdentityAuth, orgId: identityMembershipOrg.orgId }; return { ...awsIdentityAuth, orgId: identityMembershipOrg.orgId };
}; };
@@ -327,14 +330,14 @@ export const identityAwsAuthServiceFactory = ({
message: "The identity does not have aws auth" message: "The identity does not have aws auth"
}); });
} }
const { permission } = await permissionService.getOrgPermission( const { permission, membership } = await permissionService.getOrgPermission(
actor, actor,
actorId, actorId,
identityMembershipOrg.orgId, identityMembershipOrg.orgId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
const { permission: rolePermission } = await permissionService.getOrgPermission( const { permission: rolePermission } = await permissionService.getOrgPermission(
ActorType.IDENTITY, ActorType.IDENTITY,
@@ -344,11 +347,22 @@ export const identityAwsAuthServiceFactory = ({
actorOrgId actorOrgId
); );
const permissionBoundary = validatePermissionBoundary(permission, rolePermission); const permissionBoundary = validatePrivilegeChangeOperation(
membership.shouldUseNewPrivilegeSystem,
OrgPermissionIdentityActions.RevokeAuth,
OrgPermissionSubjects.Identity,
permission,
rolePermission
);
if (!permissionBoundary.isValid) if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({ throw new PermissionBoundaryError({
name: "PermissionBoundaryError", message: constructPermissionErrorMessage(
message: "Failed to revoke aws auth of identity with more privileged role", "Failed to revoke aws auth of identity with more privileged role",
membership.shouldUseNewPrivilegeSystem,
OrgPermissionIdentityActions.RevokeAuth,
OrgPermissionSubjects.Identity
),
details: { missingPermissions: permissionBoundary.missingPermissions } details: { missingPermissions: permissionBoundary.missingPermissions }
}); });
@@ -3,11 +3,14 @@ import jwt from "jsonwebtoken";
import { IdentityAuthMethod } from "@app/db/schemas"; import { IdentityAuthMethod } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import {
constructPermissionErrorMessage,
validatePrivilegeChangeOperation
} from "@app/ee/services/permission/permission-fns";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
import { ActorType, AuthTokenType } from "../auth/auth-type"; import { ActorType, AuthTokenType } from "../auth/auth-type";
@@ -147,7 +150,7 @@ export const identityAzureAuthServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Identity); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity);
const plan = await licenseService.getPlan(identityMembershipOrg.orgId); const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => { const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => {
@@ -225,7 +228,7 @@ export const identityAzureAuthServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
const plan = await licenseService.getPlan(identityMembershipOrg.orgId); const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => { const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => {
@@ -281,7 +284,7 @@ export const identityAzureAuthServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity);
return { ...identityAzureAuth, orgId: identityMembershipOrg.orgId }; return { ...identityAzureAuth, orgId: identityMembershipOrg.orgId };
}; };
@@ -300,14 +303,14 @@ export const identityAzureAuthServiceFactory = ({
message: "The identity does not have azure auth" message: "The identity does not have azure auth"
}); });
} }
const { permission } = await permissionService.getOrgPermission( const { permission, membership } = await permissionService.getOrgPermission(
actor, actor,
actorId, actorId,
identityMembershipOrg.orgId, identityMembershipOrg.orgId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
const { permission: rolePermission } = await permissionService.getOrgPermission( const { permission: rolePermission } = await permissionService.getOrgPermission(
ActorType.IDENTITY, ActorType.IDENTITY,
@@ -316,11 +319,21 @@ export const identityAzureAuthServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
const permissionBoundary = validatePermissionBoundary(permission, rolePermission); const permissionBoundary = validatePrivilegeChangeOperation(
membership.shouldUseNewPrivilegeSystem,
OrgPermissionIdentityActions.RevokeAuth,
OrgPermissionSubjects.Identity,
permission,
rolePermission
);
if (!permissionBoundary.isValid) if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({ throw new PermissionBoundaryError({
name: "PermissionBoundaryError", message: constructPermissionErrorMessage(
message: "Failed to revoke azure auth of identity with more privileged role", "Failed to revoke azure auth of identity with more privileged role",
membership.shouldUseNewPrivilegeSystem,
OrgPermissionIdentityActions.RevokeAuth,
OrgPermissionSubjects.Identity
),
details: { missingPermissions: permissionBoundary.missingPermissions } details: { missingPermissions: permissionBoundary.missingPermissions }
}); });
@@ -3,11 +3,14 @@ import jwt from "jsonwebtoken";
import { IdentityAuthMethod } from "@app/db/schemas"; import { IdentityAuthMethod } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import {
constructPermissionErrorMessage,
validatePrivilegeChangeOperation
} from "@app/ee/services/permission/permission-fns";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
import { ActorType, AuthTokenType } from "../auth/auth-type"; import { ActorType, AuthTokenType } from "../auth/auth-type";
@@ -188,7 +191,7 @@ export const identityGcpAuthServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Identity); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity);
const plan = await licenseService.getPlan(identityMembershipOrg.orgId); const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => { const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => {
@@ -268,7 +271,7 @@ export const identityGcpAuthServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
const plan = await licenseService.getPlan(identityMembershipOrg.orgId); const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => { const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => {
@@ -326,7 +329,7 @@ export const identityGcpAuthServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity);
return { ...identityGcpAuth, orgId: identityMembershipOrg.orgId }; return { ...identityGcpAuth, orgId: identityMembershipOrg.orgId };
}; };
@@ -346,14 +349,14 @@ export const identityGcpAuthServiceFactory = ({
message: "The identity does not have gcp auth" message: "The identity does not have gcp auth"
}); });
} }
const { permission } = await permissionService.getOrgPermission( const { permission, membership } = await permissionService.getOrgPermission(
actor, actor,
actorId, actorId,
identityMembershipOrg.orgId, identityMembershipOrg.orgId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
const { permission: rolePermission } = await permissionService.getOrgPermission( const { permission: rolePermission } = await permissionService.getOrgPermission(
ActorType.IDENTITY, ActorType.IDENTITY,
@@ -362,11 +365,21 @@ export const identityGcpAuthServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
const permissionBoundary = validatePermissionBoundary(permission, rolePermission); const permissionBoundary = validatePrivilegeChangeOperation(
membership.shouldUseNewPrivilegeSystem,
OrgPermissionIdentityActions.RevokeAuth,
OrgPermissionSubjects.Identity,
permission,
rolePermission
);
if (!permissionBoundary.isValid) if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({ throw new PermissionBoundaryError({
name: "PermissionBoundaryError", message: constructPermissionErrorMessage(
message: "Failed to revoke gcp auth of identity with more privileged role", "Failed to revoke gcp auth of identity with more privileged role",
membership.shouldUseNewPrivilegeSystem,
OrgPermissionIdentityActions.RevokeAuth,
OrgPermissionSubjects.Identity
),
details: { missingPermissions: permissionBoundary.missingPermissions } details: { missingPermissions: permissionBoundary.missingPermissions }
}); });
@@ -5,11 +5,20 @@ import { JwksClient } from "jwks-rsa";
import { IdentityAuthMethod, TIdentityJwtAuthsUpdate } from "@app/db/schemas"; import { IdentityAuthMethod, TIdentityJwtAuthsUpdate } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import {
constructPermissionErrorMessage,
validatePrivilegeChangeOperation
} from "@app/ee/services/permission/permission-fns";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; import {
BadRequestError,
ForbiddenRequestError,
NotFoundError,
PermissionBoundaryError,
UnauthorizedError
} from "@app/lib/errors";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
import { getStringValueByDot } from "@app/lib/template/dot-access"; import { getStringValueByDot } from "@app/lib/template/dot-access";
@@ -278,7 +287,7 @@ export const identityJwtAuthServiceFactory = ({
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Identity); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity);
const plan = await licenseService.getPlan(identityMembershipOrg.orgId); const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => { const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => {
@@ -381,7 +390,7 @@ export const identityJwtAuthServiceFactory = ({
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
const plan = await licenseService.getPlan(identityMembershipOrg.orgId); const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => { const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => {
@@ -470,7 +479,7 @@ export const identityJwtAuthServiceFactory = ({
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity);
const identityJwtAuth = await identityJwtAuthDAL.findOne({ identityId }); const identityJwtAuth = await identityJwtAuthDAL.findOne({ identityId });
@@ -504,7 +513,7 @@ export const identityJwtAuthServiceFactory = ({
}); });
} }
const { permission } = await permissionService.getOrgPermission( const { permission, membership } = await permissionService.getOrgPermission(
actor, actor,
actorId, actorId,
identityMembershipOrg.orgId, identityMembershipOrg.orgId,
@@ -512,7 +521,7 @@ export const identityJwtAuthServiceFactory = ({
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
const { permission: rolePermission } = await permissionService.getOrgPermission( const { permission: rolePermission } = await permissionService.getOrgPermission(
ActorType.IDENTITY, ActorType.IDENTITY,
@@ -522,11 +531,21 @@ export const identityJwtAuthServiceFactory = ({
actorOrgId actorOrgId
); );
const permissionBoundary = validatePermissionBoundary(permission, rolePermission); const permissionBoundary = validatePrivilegeChangeOperation(
membership.shouldUseNewPrivilegeSystem,
OrgPermissionIdentityActions.RevokeAuth,
OrgPermissionSubjects.Identity,
permission,
rolePermission
);
if (!permissionBoundary.isValid) if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({ throw new PermissionBoundaryError({
name: "PermissionBoundaryError", message: constructPermissionErrorMessage(
message: "Failed to revoke jwt auth of identity with more privileged role", "Failed to revoke jwt auth of identity with more privileged role",
membership.shouldUseNewPrivilegeSystem,
OrgPermissionIdentityActions.RevokeAuth,
OrgPermissionSubjects.Identity
),
details: { missingPermissions: permissionBoundary.missingPermissions } details: { missingPermissions: permissionBoundary.missingPermissions }
}); });
@@ -5,11 +5,14 @@ import jwt from "jsonwebtoken";
import { IdentityAuthMethod, TIdentityKubernetesAuthsUpdate } from "@app/db/schemas"; import { IdentityAuthMethod, TIdentityKubernetesAuthsUpdate } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import {
constructPermissionErrorMessage,
validatePrivilegeChangeOperation
} from "@app/ee/services/permission/permission-fns";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
import { ActorType, AuthTokenType } from "../auth/auth-type"; import { ActorType, AuthTokenType } from "../auth/auth-type";
@@ -257,7 +260,7 @@ export const identityKubernetesAuthServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Identity); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity);
const plan = await licenseService.getPlan(identityMembershipOrg.orgId); const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => { const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => {
@@ -350,7 +353,7 @@ export const identityKubernetesAuthServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
const plan = await licenseService.getPlan(identityMembershipOrg.orgId); const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => { const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => {
@@ -446,7 +449,7 @@ export const identityKubernetesAuthServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity);
const { decryptor } = await kmsService.createCipherPairWithDataKey({ const { decryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization, type: KmsDataKey.Organization,
@@ -483,14 +486,14 @@ export const identityKubernetesAuthServiceFactory = ({
message: "The identity does not have kubernetes auth" message: "The identity does not have kubernetes auth"
}); });
} }
const { permission } = await permissionService.getOrgPermission( const { permission, membership } = await permissionService.getOrgPermission(
actor, actor,
actorId, actorId,
identityMembershipOrg.orgId, identityMembershipOrg.orgId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
const { permission: rolePermission } = await permissionService.getOrgPermission( const { permission: rolePermission } = await permissionService.getOrgPermission(
ActorType.IDENTITY, ActorType.IDENTITY,
@@ -499,11 +502,21 @@ export const identityKubernetesAuthServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
const permissionBoundary = validatePermissionBoundary(permission, rolePermission); const permissionBoundary = validatePrivilegeChangeOperation(
membership.shouldUseNewPrivilegeSystem,
OrgPermissionIdentityActions.RevokeAuth,
OrgPermissionSubjects.Identity,
permission,
rolePermission
);
if (!permissionBoundary.isValid) if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({ throw new PermissionBoundaryError({
name: "PermissionBoundaryError", message: constructPermissionErrorMessage(
message: "Failed to revoke kubernetes auth of identity with more privileged role", "Failed to revoke kubernetes auth of identity with more privileged role",
membership.shouldUseNewPrivilegeSystem,
OrgPermissionIdentityActions.RevokeAuth,
OrgPermissionSubjects.Identity
),
details: { missingPermissions: permissionBoundary.missingPermissions } details: { missingPermissions: permissionBoundary.missingPermissions }
}); });
@@ -6,11 +6,20 @@ import { JwksClient } from "jwks-rsa";
import { IdentityAuthMethod, TIdentityOidcAuthsUpdate } from "@app/db/schemas"; import { IdentityAuthMethod, TIdentityOidcAuthsUpdate } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import {
constructPermissionErrorMessage,
validatePrivilegeChangeOperation
} from "@app/ee/services/permission/permission-fns";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; import {
BadRequestError,
ForbiddenRequestError,
NotFoundError,
PermissionBoundaryError,
UnauthorizedError
} from "@app/lib/errors";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
import { getStringValueByDot } from "@app/lib/template/dot-access"; import { getStringValueByDot } from "@app/lib/template/dot-access";
@@ -249,7 +258,7 @@ export const identityOidcAuthServiceFactory = ({
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Identity); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity);
const plan = await licenseService.getPlan(identityMembershipOrg.orgId); const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => { const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => {
@@ -341,7 +350,7 @@ export const identityOidcAuthServiceFactory = ({
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
const plan = await licenseService.getPlan(identityMembershipOrg.orgId); const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => { const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => {
@@ -414,7 +423,7 @@ export const identityOidcAuthServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity);
const identityOidcAuth = await identityOidcAuthDAL.findOne({ identityId }); const identityOidcAuth = await identityOidcAuthDAL.findOne({ identityId });
@@ -442,7 +451,7 @@ export const identityOidcAuthServiceFactory = ({
}); });
} }
const { permission } = await permissionService.getOrgPermission( const { permission, membership } = await permissionService.getOrgPermission(
actor, actor,
actorId, actorId,
identityMembershipOrg.orgId, identityMembershipOrg.orgId,
@@ -450,7 +459,7 @@ export const identityOidcAuthServiceFactory = ({
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
const { permission: rolePermission } = await permissionService.getOrgPermission( const { permission: rolePermission } = await permissionService.getOrgPermission(
ActorType.IDENTITY, ActorType.IDENTITY,
@@ -460,11 +469,22 @@ export const identityOidcAuthServiceFactory = ({
actorOrgId actorOrgId
); );
const permissionBoundary = validatePermissionBoundary(permission, rolePermission); const permissionBoundary = validatePrivilegeChangeOperation(
membership.shouldUseNewPrivilegeSystem,
OrgPermissionIdentityActions.RevokeAuth,
OrgPermissionSubjects.Identity,
permission,
rolePermission
);
if (!permissionBoundary.isValid) if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({ throw new PermissionBoundaryError({
name: "PermissionBoundaryError", message: constructPermissionErrorMessage(
message: "Failed to revoke oidc auth of identity with more privileged role", "Failed to revoke oidc auth of identity with more privileged role",
membership.shouldUseNewPrivilegeSystem,
OrgPermissionIdentityActions.RevokeAuth,
OrgPermissionSubjects.Identity
),
details: { missingPermissions: permissionBoundary.missingPermissions } details: { missingPermissions: permissionBoundary.missingPermissions }
}); });
@@ -1,14 +1,16 @@
import { ForbiddenError, subject } from "@casl/ability"; import { ForbiddenError, subject } from "@casl/ability";
import { ActionProjectType, ProjectMembershipRole } from "@app/db/schemas"; import { ActionProjectType, ProjectMembershipRole } from "@app/db/schemas";
import {
constructPermissionErrorMessage,
validatePrivilegeChangeOperation
} from "@app/ee/services/permission/permission-fns";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionIdentityActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { validatePermissionBoundary } from "@app/lib/casl/boundary"; import { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { groupBy } from "@app/lib/fn"; import { groupBy } from "@app/lib/fn";
import { ms } from "@app/lib/ms"; import { ms } from "@app/lib/ms";
import { ActorType } from "../auth/auth-type";
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
import { TProjectDALFactory } from "../project/project-dal"; import { TProjectDALFactory } from "../project/project-dal";
import { ProjectUserMembershipTemporaryMode } from "../project-membership/project-membership-types"; import { ProjectUserMembershipTemporaryMode } from "../project-membership/project-membership-types";
@@ -54,7 +56,7 @@ export const identityProjectServiceFactory = ({
projectId, projectId,
roles roles
}: TCreateProjectIdentityDTO) => { }: TCreateProjectIdentityDTO) => {
const { permission } = await permissionService.getProjectPermission({ const { permission, membership } = await permissionService.getProjectPermission({
actor, actor,
actorId, actorId,
projectId, projectId,
@@ -63,7 +65,7 @@ export const identityProjectServiceFactory = ({
actionProjectType: ActionProjectType.Any actionProjectType: ActionProjectType.Any
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create, ProjectPermissionIdentityActions.Create,
subject(ProjectPermissionSub.Identity, { subject(ProjectPermissionSub.Identity, {
identityId identityId
}) })
@@ -91,11 +93,21 @@ export const identityProjectServiceFactory = ({
projectId projectId
); );
const permissionBoundary = validatePermissionBoundary(permission, rolePermission); const permissionBoundary = validatePrivilegeChangeOperation(
membership.shouldUseNewPrivilegeSystem,
ProjectPermissionIdentityActions.GrantPrivileges,
ProjectPermissionSub.Identity,
permission,
rolePermission
);
if (!permissionBoundary.isValid) if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({ throw new PermissionBoundaryError({
name: "PermissionBoundaryError", message: constructPermissionErrorMessage(
message: "Failed to assign to a more privileged role", "Failed to assign to role",
membership.shouldUseNewPrivilegeSystem,
ProjectPermissionIdentityActions.GrantPrivileges,
ProjectPermissionSub.Identity
),
details: { missingPermissions: permissionBoundary.missingPermissions } details: { missingPermissions: permissionBoundary.missingPermissions }
}); });
} }
@@ -162,7 +174,7 @@ export const identityProjectServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
}: TUpdateProjectIdentityDTO) => { }: TUpdateProjectIdentityDTO) => {
const { permission } = await permissionService.getProjectPermission({ const { permission, membership } = await permissionService.getProjectPermission({
actor, actor,
actorId, actorId,
projectId, projectId,
@@ -171,7 +183,7 @@ export const identityProjectServiceFactory = ({
actionProjectType: ActionProjectType.Any actionProjectType: ActionProjectType.Any
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit, ProjectPermissionIdentityActions.Edit,
subject(ProjectPermissionSub.Identity, { identityId }) subject(ProjectPermissionSub.Identity, { identityId })
); );
@@ -187,11 +199,22 @@ export const identityProjectServiceFactory = ({
projectId projectId
); );
const permissionBoundary = validatePermissionBoundary(permission, rolePermission); const permissionBoundary = validatePrivilegeChangeOperation(
membership.shouldUseNewPrivilegeSystem,
ProjectPermissionIdentityActions.GrantPrivileges,
ProjectPermissionSub.Identity,
permission,
rolePermission
);
if (!permissionBoundary.isValid) if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({ throw new PermissionBoundaryError({
name: "PermissionBoundaryError", message: constructPermissionErrorMessage(
message: "Failed to change to a more privileged role", "Failed to change role",
membership.shouldUseNewPrivilegeSystem,
ProjectPermissionIdentityActions.GrantPrivileges,
ProjectPermissionSub.Identity
),
details: { missingPermissions: permissionBoundary.missingPermissions } details: { missingPermissions: permissionBoundary.missingPermissions }
}); });
} }
@@ -271,26 +294,10 @@ export const identityProjectServiceFactory = ({
actionProjectType: ActionProjectType.Any actionProjectType: ActionProjectType.Any
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete, ProjectPermissionIdentityActions.Delete,
subject(ProjectPermissionSub.Identity, { identityId }) subject(ProjectPermissionSub.Identity, { identityId })
); );
const { permission: identityRolePermission } = await permissionService.getProjectPermission({
actor: ActorType.IDENTITY,
actorId: identityId,
projectId: identityProjectMembership.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.Any
});
const permissionBoundary = validatePermissionBoundary(permission, identityRolePermission);
if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({
name: "PermissionBoundaryError",
message: "Failed to remove more privileged identity",
details: { missingPermissions: permissionBoundary.missingPermissions }
});
const [deletedIdentity] = await identityProjectDAL.delete({ identityId, projectId }); const [deletedIdentity] = await identityProjectDAL.delete({ identityId, projectId });
return deletedIdentity; return deletedIdentity;
}; };
@@ -315,7 +322,10 @@ export const identityProjectServiceFactory = ({
actorOrgId, actorOrgId,
actionProjectType: ActionProjectType.Any actionProjectType: ActionProjectType.Any
}); });
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Identity); ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionIdentityActions.Read,
ProjectPermissionSub.Identity
);
const identityMemberships = await identityProjectDAL.findByProjectId(projectId, { const identityMemberships = await identityProjectDAL.findByProjectId(projectId, {
limit, limit,
@@ -348,7 +358,7 @@ export const identityProjectServiceFactory = ({
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read, ProjectPermissionIdentityActions.Read,
subject(ProjectPermissionSub.Identity, { identityId }) subject(ProjectPermissionSub.Identity, { identityId })
); );
@@ -3,11 +3,14 @@ import jwt from "jsonwebtoken";
import { IdentityAuthMethod, TableName } from "@app/db/schemas"; import { IdentityAuthMethod, TableName } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import {
constructPermissionErrorMessage,
validatePrivilegeChangeOperation
} from "@app/ee/services/permission/permission-fns";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
import { ActorType, AuthTokenType } from "../auth/auth-type"; import { ActorType, AuthTokenType } from "../auth/auth-type";
@@ -85,7 +88,7 @@ export const identityTokenAuthServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Identity); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity);
const plan = await licenseService.getPlan(identityMembershipOrg.orgId); const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => { const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => {
@@ -161,7 +164,7 @@ export const identityTokenAuthServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
const plan = await licenseService.getPlan(identityMembershipOrg.orgId); const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => { const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => {
@@ -215,7 +218,7 @@ export const identityTokenAuthServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity);
return { ...identityTokenAuth, orgId: identityMembershipOrg.orgId }; return { ...identityTokenAuth, orgId: identityMembershipOrg.orgId };
}; };
@@ -245,9 +248,9 @@ export const identityTokenAuthServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
const { permission: rolePermission } = await permissionService.getOrgPermission( const { permission: rolePermission, membership } = await permissionService.getOrgPermission(
ActorType.IDENTITY, ActorType.IDENTITY,
identityMembershipOrg.identityId, identityMembershipOrg.identityId,
identityMembershipOrg.orgId, identityMembershipOrg.orgId,
@@ -255,11 +258,21 @@ export const identityTokenAuthServiceFactory = ({
actorOrgId actorOrgId
); );
const permissionBoundary = validatePermissionBoundary(permission, rolePermission); const permissionBoundary = validatePrivilegeChangeOperation(
membership.shouldUseNewPrivilegeSystem,
OrgPermissionIdentityActions.RevokeAuth,
OrgPermissionSubjects.Identity,
permission,
rolePermission
);
if (!permissionBoundary.isValid) if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({ throw new PermissionBoundaryError({
name: "PermissionBoundaryError", message: constructPermissionErrorMessage(
message: "Failed to revoke token auth of identity with more privileged role", "Failed to revoke token auth of identity with more privileged role",
membership.shouldUseNewPrivilegeSystem,
OrgPermissionIdentityActions.RevokeAuth,
OrgPermissionSubjects.Identity
),
details: { missingPermissions: permissionBoundary.missingPermissions } details: { missingPermissions: permissionBoundary.missingPermissions }
}); });
@@ -301,20 +314,31 @@ export const identityTokenAuthServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
const { permission: rolePermission } = await permissionService.getOrgPermission( const { permission: rolePermission, membership } = await permissionService.getOrgPermission(
ActorType.IDENTITY, ActorType.IDENTITY,
identityMembershipOrg.identityId, identityMembershipOrg.identityId,
identityMembershipOrg.orgId, identityMembershipOrg.orgId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
const permissionBoundary = validatePrivilegeChangeOperation(
membership.shouldUseNewPrivilegeSystem,
OrgPermissionIdentityActions.CreateToken,
OrgPermissionSubjects.Identity,
permission,
rolePermission
);
if (!permissionBoundary.isValid) if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({ throw new PermissionBoundaryError({
name: "PermissionBoundaryError", message: constructPermissionErrorMessage(
message: "Failed to create token for identity with more privileged role", "Failed to create token for identity with more privileged role",
membership.shouldUseNewPrivilegeSystem,
OrgPermissionIdentityActions.CreateToken,
OrgPermissionSubjects.Identity
),
details: { missingPermissions: permissionBoundary.missingPermissions } details: { missingPermissions: permissionBoundary.missingPermissions }
}); });
@@ -383,7 +407,7 @@ export const identityTokenAuthServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity);
const tokens = await identityAccessTokenDAL.find( const tokens = await identityAccessTokenDAL.find(
{ {
@@ -429,20 +453,30 @@ export const identityTokenAuthServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
const { permission: rolePermission } = await permissionService.getOrgPermission( const { permission: rolePermission, membership } = await permissionService.getOrgPermission(
ActorType.IDENTITY, ActorType.IDENTITY,
identityMembershipOrg.identityId, identityMembershipOrg.identityId,
identityMembershipOrg.orgId, identityMembershipOrg.orgId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
const permissionBoundary = validatePermissionBoundary(permission, rolePermission); const permissionBoundary = validatePrivilegeChangeOperation(
membership.shouldUseNewPrivilegeSystem,
OrgPermissionIdentityActions.CreateToken,
OrgPermissionSubjects.Identity,
permission,
rolePermission
);
if (!permissionBoundary.isValid) if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({ throw new PermissionBoundaryError({
name: "PermissionBoundaryError", message: constructPermissionErrorMessage(
message: "Failed to update token for identity with more privileged role", "Failed to update token for identity with more privileged role",
membership.shouldUseNewPrivilegeSystem,
OrgPermissionIdentityActions.CreateToken,
OrgPermissionSubjects.Identity
),
details: { missingPermissions: permissionBoundary.missingPermissions } details: { missingPermissions: permissionBoundary.missingPermissions }
}); });
@@ -496,7 +530,7 @@ export const identityTokenAuthServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
const [revokedToken] = await identityAccessTokenDAL.update( const [revokedToken] = await identityAccessTokenDAL.update(
{ {
@@ -6,11 +6,14 @@ import jwt from "jsonwebtoken";
import { IdentityAuthMethod } from "@app/db/schemas"; import { IdentityAuthMethod } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import {
constructPermissionErrorMessage,
validatePrivilegeChangeOperation
} from "@app/ee/services/permission/permission-fns";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors";
import { checkIPAgainstBlocklist, extractIPDetails, isValidIpOrCidr, TIp } from "@app/lib/ip"; import { checkIPAgainstBlocklist, extractIPDetails, isValidIpOrCidr, TIp } from "@app/lib/ip";
import { ActorType, AuthTokenType } from "../auth/auth-type"; import { ActorType, AuthTokenType } from "../auth/auth-type";
@@ -184,7 +187,7 @@ export const identityUaServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Identity); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity);
const plan = await licenseService.getPlan(identityMembershipOrg.orgId); const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
const reformattedClientSecretTrustedIps = clientSecretTrustedIps.map((clientSecretTrustedIp) => { const reformattedClientSecretTrustedIps = clientSecretTrustedIps.map((clientSecretTrustedIp) => {
@@ -278,7 +281,7 @@ export const identityUaServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
const plan = await licenseService.getPlan(identityMembershipOrg.orgId); const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
const reformattedClientSecretTrustedIps = clientSecretTrustedIps?.map((clientSecretTrustedIp) => { const reformattedClientSecretTrustedIps = clientSecretTrustedIps?.map((clientSecretTrustedIp) => {
@@ -350,7 +353,7 @@ export const identityUaServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity);
return { ...uaIdentityAuth, orgId: identityMembershipOrg.orgId }; return { ...uaIdentityAuth, orgId: identityMembershipOrg.orgId };
}; };
@@ -376,20 +379,30 @@ export const identityUaServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
const { permission: rolePermission } = await permissionService.getOrgPermission( const { permission: rolePermission, membership } = await permissionService.getOrgPermission(
ActorType.IDENTITY, ActorType.IDENTITY,
identityMembershipOrg.identityId, identityMembershipOrg.identityId,
identityMembershipOrg.orgId, identityMembershipOrg.orgId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
const permissionBoundary = validatePermissionBoundary(permission, rolePermission); const permissionBoundary = validatePrivilegeChangeOperation(
membership.shouldUseNewPrivilegeSystem,
OrgPermissionIdentityActions.RevokeAuth,
OrgPermissionSubjects.Identity,
permission,
rolePermission
);
if (!permissionBoundary.isValid) if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({ throw new PermissionBoundaryError({
name: "PermissionBoundaryError", message: constructPermissionErrorMessage(
message: "Failed to revoke universal auth of identity with more privileged role", "Failed to revoke universal auth of identity with more privileged role",
membership.shouldUseNewPrivilegeSystem,
OrgPermissionIdentityActions.RevokeAuth,
OrgPermissionSubjects.Identity
),
details: { missingPermissions: permissionBoundary.missingPermissions } details: { missingPermissions: permissionBoundary.missingPermissions }
}); });
@@ -419,14 +432,14 @@ export const identityUaServiceFactory = ({
}); });
} }
const { permission } = await permissionService.getOrgPermission( const { permission, membership } = await permissionService.getOrgPermission(
actor, actor,
actorId, actorId,
identityMembershipOrg.orgId, identityMembershipOrg.orgId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Identity); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity);
const { permission: rolePermission } = await permissionService.getOrgPermission( const { permission: rolePermission } = await permissionService.getOrgPermission(
ActorType.IDENTITY, ActorType.IDENTITY,
@@ -435,11 +448,21 @@ export const identityUaServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
const permissionBoundary = validatePermissionBoundary(permission, rolePermission); const permissionBoundary = validatePrivilegeChangeOperation(
membership.shouldUseNewPrivilegeSystem,
OrgPermissionIdentityActions.CreateToken,
OrgPermissionSubjects.Identity,
permission,
rolePermission
);
if (!permissionBoundary.isValid) if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({ throw new PermissionBoundaryError({
name: "PermissionBoundaryError", message: constructPermissionErrorMessage(
message: "Failed to create client secret for a more privileged identity.", "Failed to create client secret for identity.",
membership.shouldUseNewPrivilegeSystem,
OrgPermissionIdentityActions.CreateToken,
OrgPermissionSubjects.Identity
),
details: { missingPermissions: permissionBoundary.missingPermissions } details: { missingPermissions: permissionBoundary.missingPermissions }
}); });
@@ -481,14 +504,14 @@ export const identityUaServiceFactory = ({
message: "The identity does not have universal auth" message: "The identity does not have universal auth"
}); });
} }
const { permission } = await permissionService.getOrgPermission( const { permission, membership } = await permissionService.getOrgPermission(
actor, actor,
actorId, actorId,
identityMembershipOrg.orgId, identityMembershipOrg.orgId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity);
const { permission: rolePermission } = await permissionService.getOrgPermission( const { permission: rolePermission } = await permissionService.getOrgPermission(
ActorType.IDENTITY, ActorType.IDENTITY,
@@ -498,11 +521,21 @@ export const identityUaServiceFactory = ({
actorOrgId actorOrgId
); );
const permissionBoundary = validatePermissionBoundary(permission, rolePermission); const permissionBoundary = validatePrivilegeChangeOperation(
membership.shouldUseNewPrivilegeSystem,
OrgPermissionIdentityActions.GetToken,
OrgPermissionSubjects.Identity,
permission,
rolePermission
);
if (!permissionBoundary.isValid) if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({ throw new PermissionBoundaryError({
name: "PermissionBoundaryError", message: constructPermissionErrorMessage(
message: "Failed to get identity client secret with more privileged role", "Failed to get identity client secret with more privileged role",
membership.shouldUseNewPrivilegeSystem,
OrgPermissionIdentityActions.GetToken,
OrgPermissionSubjects.Identity
),
details: { missingPermissions: permissionBoundary.missingPermissions } details: { missingPermissions: permissionBoundary.missingPermissions }
}); });
@@ -534,14 +567,14 @@ export const identityUaServiceFactory = ({
}); });
} }
const { permission } = await permissionService.getOrgPermission( const { permission, membership } = await permissionService.getOrgPermission(
actor, actor,
actorId, actorId,
identityMembershipOrg.orgId, identityMembershipOrg.orgId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity);
const { permission: rolePermission } = await permissionService.getOrgPermission( const { permission: rolePermission } = await permissionService.getOrgPermission(
ActorType.IDENTITY, ActorType.IDENTITY,
@@ -550,11 +583,21 @@ export const identityUaServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
const permissionBoundary = validatePermissionBoundary(permission, rolePermission); const permissionBoundary = validatePrivilegeChangeOperation(
membership.shouldUseNewPrivilegeSystem,
OrgPermissionIdentityActions.GetToken,
OrgPermissionSubjects.Identity,
permission,
rolePermission
);
if (!permissionBoundary.isValid) if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({ throw new PermissionBoundaryError({
name: "PermissionBoundaryError", message: constructPermissionErrorMessage(
message: "Failed to read identity client secret of identity with more privileged role", "Failed to read identity client secret of identity with more privileged role",
membership.shouldUseNewPrivilegeSystem,
OrgPermissionIdentityActions.GetToken,
OrgPermissionSubjects.Identity
),
details: { missingPermissions: permissionBoundary.missingPermissions } details: { missingPermissions: permissionBoundary.missingPermissions }
}); });
@@ -579,14 +622,14 @@ export const identityUaServiceFactory = ({
}); });
} }
const { permission } = await permissionService.getOrgPermission( const { permission, membership } = await permissionService.getOrgPermission(
actor, actor,
actorId, actorId,
identityMembershipOrg.orgId, identityMembershipOrg.orgId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Identity); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Delete, OrgPermissionSubjects.Identity);
const { permission: rolePermission } = await permissionService.getOrgPermission( const { permission: rolePermission } = await permissionService.getOrgPermission(
ActorType.IDENTITY, ActorType.IDENTITY,
@@ -595,17 +638,30 @@ export const identityUaServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
if (!permissionBoundary.isValid) const permissionBoundary = validatePrivilegeChangeOperation(
throw new ForbiddenRequestError({ membership.shouldUseNewPrivilegeSystem,
name: "PermissionBoundaryError", OrgPermissionIdentityActions.DeleteToken,
message: "Failed to revoke identity client secret with more privileged role", OrgPermissionSubjects.Identity,
permission,
rolePermission
);
if (!permissionBoundary.isValid) {
throw new PermissionBoundaryError({
message: constructPermissionErrorMessage(
"Failed to revoke identity client secret with more privileged role",
membership.shouldUseNewPrivilegeSystem,
OrgPermissionIdentityActions.DeleteToken,
OrgPermissionSubjects.Identity
),
details: { missingPermissions: permissionBoundary.missingPermissions } details: { missingPermissions: permissionBoundary.missingPermissions }
}); });
}
const clientSecret = await identityUaClientSecretDAL.updateById(clientSecretId, { const clientSecret = await identityUaClientSecretDAL.updateById(clientSecretId, {
isClientSecretRevoked: true isClientSecretRevoked: true
}); });
return { ...clientSecret, identityId, orgId: identityMembershipOrg.orgId }; return { ...clientSecret, identityId, orgId: identityMembershipOrg.orgId };
}; };
@@ -2,13 +2,15 @@ import { ForbiddenError } from "@casl/ability";
import { OrgMembershipRole, TableName, TOrgRoles } from "@app/db/schemas"; import { OrgMembershipRole, TableName, TOrgRoles } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import {
constructPermissionErrorMessage,
validatePrivilegeChangeOperation
} from "@app/ee/services/permission/permission-fns";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { validatePermissionBoundary } from "@app/lib/casl/boundary"; import { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal"; import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal";
import { ActorType } from "../auth/auth-type";
import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns"; import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns";
import { TIdentityDALFactory } from "./identity-dal"; import { TIdentityDALFactory } from "./identity-dal";
import { TIdentityMetadataDALFactory } from "./identity-metadata-dal"; import { TIdentityMetadataDALFactory } from "./identity-metadata-dal";
@@ -51,19 +53,35 @@ export const identityServiceFactory = ({
actorOrgId, actorOrgId,
metadata metadata
}: TCreateIdentityDTO) => { }: TCreateIdentityDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); const { permission, membership } = await permissionService.getOrgPermission(
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Identity); actor,
actorId,
orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity);
const { permission: rolePermission, role: customRole } = await permissionService.getOrgPermissionByRole( const { permission: rolePermission, role: customRole } = await permissionService.getOrgPermissionByRole(
role, role,
orgId orgId
); );
const isCustomRole = Boolean(customRole); const isCustomRole = Boolean(customRole);
const permissionBoundary = validatePermissionBoundary(permission, rolePermission); const permissionBoundary = validatePrivilegeChangeOperation(
membership.shouldUseNewPrivilegeSystem,
OrgPermissionIdentityActions.GrantPrivileges,
OrgPermissionSubjects.Identity,
permission,
rolePermission
);
if (!permissionBoundary.isValid) if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({ throw new PermissionBoundaryError({
name: "PermissionBoundaryError", message: constructPermissionErrorMessage(
message: "Failed to create a more privileged identity", "Failed to create identity",
membership.shouldUseNewPrivilegeSystem,
OrgPermissionIdentityActions.GrantPrivileges,
OrgPermissionSubjects.Identity
),
details: { missingPermissions: permissionBoundary.missingPermissions } details: { missingPermissions: permissionBoundary.missingPermissions }
}); });
@@ -121,29 +139,14 @@ export const identityServiceFactory = ({
const identityOrgMembership = await identityOrgMembershipDAL.findOne({ identityId: id }); const identityOrgMembership = await identityOrgMembershipDAL.findOne({ identityId: id });
if (!identityOrgMembership) throw new NotFoundError({ message: `Failed to find identity with id ${id}` }); if (!identityOrgMembership) throw new NotFoundError({ message: `Failed to find identity with id ${id}` });
const { permission } = await permissionService.getOrgPermission( const { permission, membership } = await permissionService.getOrgPermission(
actor, actor,
actorId, actorId,
identityOrgMembership.orgId, identityOrgMembership.orgId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
const { permission: identityRolePermission } = await permissionService.getOrgPermission(
ActorType.IDENTITY,
id,
identityOrgMembership.orgId,
actorAuthMethod,
actorOrgId
);
const permissionBoundary = validatePermissionBoundary(permission, identityRolePermission);
if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({
name: "PermissionBoundaryError",
message: "Failed to update a more privileged identity",
details: { missingPermissions: permissionBoundary.missingPermissions }
});
let customRole: TOrgRoles | undefined; let customRole: TOrgRoles | undefined;
if (role) { if (role) {
@@ -153,11 +156,21 @@ export const identityServiceFactory = ({
); );
const isCustomRole = Boolean(customOrgRole); const isCustomRole = Boolean(customOrgRole);
const appliedRolePermissionBoundary = validatePermissionBoundary(permission, rolePermission); const appliedRolePermissionBoundary = validatePrivilegeChangeOperation(
membership.shouldUseNewPrivilegeSystem,
OrgPermissionIdentityActions.GrantPrivileges,
OrgPermissionSubjects.Identity,
permission,
rolePermission
);
if (!appliedRolePermissionBoundary.isValid) if (!appliedRolePermissionBoundary.isValid)
throw new ForbiddenRequestError({ throw new PermissionBoundaryError({
name: "PermissionBoundaryError", message: constructPermissionErrorMessage(
message: "Failed to create a more privileged identity", "Failed to update identity",
membership.shouldUseNewPrivilegeSystem,
OrgPermissionIdentityActions.GrantPrivileges,
OrgPermissionSubjects.Identity
),
details: { missingPermissions: appliedRolePermissionBoundary.missingPermissions } details: { missingPermissions: appliedRolePermissionBoundary.missingPermissions }
}); });
if (isCustomRole) customRole = customOrgRole; if (isCustomRole) customRole = customOrgRole;
@@ -209,7 +222,7 @@ export const identityServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity);
return identity; return identity;
}; };
@@ -233,21 +246,8 @@ export const identityServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Identity);
const { permission: identityRolePermission } = await permissionService.getOrgPermission( ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Delete, OrgPermissionSubjects.Identity);
ActorType.IDENTITY,
id,
identityOrgMembership.orgId,
actorAuthMethod,
actorOrgId
);
const permissionBoundary = validatePermissionBoundary(permission, identityRolePermission);
if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({
name: "PermissionBoundaryError",
message: "Failed to delete more privileged identity",
details: { missingPermissions: permissionBoundary.missingPermissions }
});
const deletedIdentity = await identityDAL.deleteById(id); const deletedIdentity = await identityDAL.deleteById(id);
@@ -269,7 +269,7 @@ export const identityServiceFactory = ({
search search
}: TListOrgIdentitiesByOrgIdDTO) => { }: TListOrgIdentitiesByOrgIdDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity);
const identityMemberships = await identityOrgMembershipDAL.find({ const identityMemberships = await identityOrgMembershipDAL.find({
[`${TableName.IdentityOrgMembership}.orgId` as "orgId"]: orgId, [`${TableName.IdentityOrgMembership}.orgId` as "orgId"]: orgId,
@@ -305,7 +305,7 @@ export const identityServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity);
const identityMemberships = await identityProjectDAL.findByIdentityId(identityId); const identityMemberships = await identityProjectDAL.findByIdentityId(identityId);
return identityMemberships; return identityMemberships;
+5 -1
View File
@@ -12,5 +12,9 @@ export const sanitizedOrganizationSchema = OrganizationsSchema.pick({
kmsDefaultKeyId: true, kmsDefaultKeyId: true,
defaultMembershipRole: true, defaultMembershipRole: true,
enforceMfa: true, enforceMfa: true,
selectedMfaMethod: true selectedMfaMethod: true,
allowSecretSharingOutsideOrganization: true,
shouldUseNewPrivilegeSystem: true,
privilegeUpgradeInitiatedByUsername: true,
privilegeUpgradeInitiatedAt: true
}); });
+88 -8
View File
@@ -21,18 +21,29 @@ import { TLicenseServiceFactory } from "@app/ee/services/license/license-service
import { TOidcConfigDALFactory } from "@app/ee/services/oidc/oidc-config-dal"; import { TOidcConfigDALFactory } from "@app/ee/services/oidc/oidc-config-dal";
import { import {
OrgPermissionActions, OrgPermissionActions,
OrgPermissionGroupActions,
OrgPermissionSecretShareAction, OrgPermissionSecretShareAction,
OrgPermissionSubjects OrgPermissionSubjects
} from "@app/ee/services/permission/org-permission"; } from "@app/ee/services/permission/org-permission";
import {
constructPermissionErrorMessage,
validatePrivilegeChangeOperation
} from "@app/ee/services/permission/permission-fns";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionMemberActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { TProjectUserAdditionalPrivilegeDALFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal"; import { TProjectUserAdditionalPrivilegeDALFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal";
import { TSamlConfigDALFactory } from "@app/ee/services/saml-config/saml-config-dal"; import { TSamlConfigDALFactory } from "@app/ee/services/saml-config/saml-config-dal";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { generateAsymmetricKeyPair } from "@app/lib/crypto"; import { generateAsymmetricKeyPair } from "@app/lib/crypto";
import { generateSymmetricKey, infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; import { generateSymmetricKey, infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { generateUserSrpKeys } from "@app/lib/crypto/srp"; import { generateUserSrpKeys } from "@app/lib/crypto/srp";
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; import {
BadRequestError,
ForbiddenRequestError,
NotFoundError,
PermissionBoundaryError,
UnauthorizedError
} from "@app/lib/errors";
import { groupBy } from "@app/lib/fn"; import { groupBy } from "@app/lib/fn";
import { alphaNumericNanoId } from "@app/lib/nanoid"; import { alphaNumericNanoId } from "@app/lib/nanoid";
import { isDisposableEmail } from "@app/lib/validator"; import { isDisposableEmail } from "@app/lib/validator";
@@ -76,6 +87,7 @@ import {
TResendOrgMemberInvitationDTO, TResendOrgMemberInvitationDTO,
TUpdateOrgDTO, TUpdateOrgDTO,
TUpdateOrgMembershipDTO, TUpdateOrgMembershipDTO,
TUpgradePrivilegeSystemDTO,
TVerifyUserToOrgDTO TVerifyUserToOrgDTO
} from "./org-types"; } from "./org-types";
@@ -187,7 +199,7 @@ export const orgServiceFactory = ({
const getOrgGroups = async ({ actor, actorId, orgId, actorAuthMethod, actorOrgId }: TGetOrgGroupsDTO) => { const getOrgGroups = async ({ actor, actorId, orgId, actorAuthMethod, actorOrgId }: TGetOrgGroupsDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Groups); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Read, OrgPermissionSubjects.Groups);
const groups = await groupDAL.findByOrgId(orgId); const groups = await groupDAL.findByOrgId(orgId);
return groups; return groups;
}; };
@@ -281,6 +293,45 @@ export const orgServiceFactory = ({
}; };
}; };
const upgradePrivilegeSystem = async ({
actorId,
actorOrgId,
actorAuthMethod,
orgId
}: TUpgradePrivilegeSystemDTO) => {
const { membership } = await permissionService.getUserOrgPermission(actorId, orgId, actorAuthMethod, actorOrgId);
if (membership.role !== OrgMembershipRole.Admin) {
throw new ForbiddenRequestError({
message: "Insufficient privileges - only the organization admin can upgrade the privilege system."
});
}
return orgDAL.transaction(async (tx) => {
const org = await orgDAL.findById(actorOrgId, tx);
if (org.shouldUseNewPrivilegeSystem) {
throw new BadRequestError({
message: "Privilege system already upgraded"
});
}
const user = await userDAL.findById(actorId, tx);
if (!user) {
throw new NotFoundError({ message: `User with ID '${actorId}' not found` });
}
return orgDAL.updateById(
actorOrgId,
{
shouldUseNewPrivilegeSystem: true,
privilegeUpgradeInitiatedAt: new Date(),
privilegeUpgradeInitiatedByUsername: user.username
},
tx
);
});
};
/* /*
* Update organization details * Update organization details
* */ * */
@@ -856,7 +907,7 @@ export const orgServiceFactory = ({
// if there exist no project membership we set is as given by the request // if there exist no project membership we set is as given by the request
for await (const project of projectsToInvite) { for await (const project of projectsToInvite) {
const projectId = project.id; const projectId = project.id;
const { permission: projectPermission } = await permissionService.getProjectPermission({ const { permission: projectPermission, membership } = await permissionService.getProjectPermission({
actor, actor,
actorId, actorId,
projectId, projectId,
@@ -865,7 +916,7 @@ export const orgServiceFactory = ({
actionProjectType: ActionProjectType.Any actionProjectType: ActionProjectType.Any
}); });
ForbiddenError.from(projectPermission).throwUnlessCan( ForbiddenError.from(projectPermission).throwUnlessCan(
ProjectPermissionActions.Create, ProjectPermissionMemberActions.Create,
ProjectPermissionSub.Member ProjectPermissionSub.Member
); );
const existingMembers = await projectMembershipDAL.find( const existingMembers = await projectMembershipDAL.find(
@@ -888,6 +939,34 @@ export const orgServiceFactory = ({
ProjectMembershipRole.Member ProjectMembershipRole.Member
]; ];
for await (const invitedRole of invitedProjectRoles) {
const { permission: rolePermission } = await permissionService.getProjectPermissionByRole(
invitedRole,
projectId
);
if (invitedRole !== ProjectMembershipRole.NoAccess) {
const permissionBoundary = validatePrivilegeChangeOperation(
membership.shouldUseNewPrivilegeSystem,
ProjectPermissionMemberActions.GrantPrivileges,
ProjectPermissionSub.Member,
projectPermission,
rolePermission
);
if (!permissionBoundary.isValid)
throw new PermissionBoundaryError({
message: constructPermissionErrorMessage(
"Failed to invite user to the project",
membership.shouldUseNewPrivilegeSystem,
ProjectPermissionMemberActions.GrantPrivileges,
ProjectPermissionSub.Member
),
details: { missingPermissions: permissionBoundary.missingPermissions }
});
}
}
const customProjectRoles = invitedProjectRoles.filter( const customProjectRoles = invitedProjectRoles.filter(
(role) => !Object.values(ProjectMembershipRole).includes(role as ProjectMembershipRole) (role) => !Object.values(ProjectMembershipRole).includes(role as ProjectMembershipRole)
); );
@@ -1021,9 +1100,9 @@ export const orgServiceFactory = ({
const sanitizedProjectMembershipRoles: TProjectUserMembershipRolesInsert[] = []; const sanitizedProjectMembershipRoles: TProjectUserMembershipRolesInsert[] = [];
invitedProjectRoles.forEach((projectRole) => { invitedProjectRoles.forEach((projectRole) => {
const isCustomRole = Boolean(customRolesGroupBySlug?.[projectRole]?.[0]); const isCustomRole = Boolean(customRolesGroupBySlug?.[projectRole]?.[0]);
projectMemberships.forEach((membership) => { projectMemberships.forEach((membershipEntry) => {
sanitizedProjectMembershipRoles.push({ sanitizedProjectMembershipRoles.push({
projectMembershipId: membership.id, projectMembershipId: membershipEntry.id,
role: isCustomRole ? ProjectMembershipRole.Custom : projectRole, role: isCustomRole ? ProjectMembershipRole.Custom : projectRole,
customRoleId: customRolesGroupBySlug[projectRole] ? customRolesGroupBySlug[projectRole][0].id : null customRoleId: customRolesGroupBySlug[projectRole] ? customRolesGroupBySlug[projectRole][0].id : null
}); });
@@ -1304,6 +1383,7 @@ export const orgServiceFactory = ({
getOrgGroups, getOrgGroups,
listProjectMembershipsByOrgMembershipId, listProjectMembershipsByOrgMembershipId,
findOrgBySlug, findOrgBySlug,
resendOrgMemberInvitation resendOrgMemberInvitation,
upgradePrivilegeSystem
}; };
}; };
+2
View File
@@ -76,6 +76,8 @@ export type TUpdateOrgDTO = {
}>; }>;
} & TOrgPermission; } & TOrgPermission;
export type TUpgradePrivilegeSystemDTO = Omit<TOrgPermission, "actor">;
export type TGetOrgGroupsDTO = TOrgPermission; export type TGetOrgGroupsDTO = TOrgPermission;
export type TListProjectMembershipsByOrgMembershipIdDTO = { export type TListProjectMembershipsByOrgMembershipIdDTO = {
@@ -2,7 +2,7 @@ import { ForbiddenError } from "@casl/ability";
import { ActionProjectType } from "@app/db/schemas"; import { ActionProjectType } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionMemberActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal"; import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
@@ -40,7 +40,7 @@ export const projectKeyServiceFactory = ({
actorOrgId, actorOrgId,
actionProjectType: ActionProjectType.Any actionProjectType: ActionProjectType.Any
}); });
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Edit, ProjectPermissionSub.Member);
const receiverMembership = await projectMembershipDAL.findOne({ const receiverMembership = await projectMembershipDAL.findOne({
userId: receiverId, userId: receiverId,
@@ -89,7 +89,7 @@ export const projectKeyServiceFactory = ({
actorOrgId, actorOrgId,
actionProjectType: ActionProjectType.Any actionProjectType: ActionProjectType.Any
}); });
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Member); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Read, ProjectPermissionSub.Member);
return projectKeyDAL.findAllProjectUserPubKeys(projectId); return projectKeyDAL.findAllProjectUserPubKeys(projectId);
}; };
@@ -3,12 +3,15 @@ import { ForbiddenError } from "@casl/ability";
import { ActionProjectType, ProjectMembershipRole, ProjectVersion, TableName } from "@app/db/schemas"; import { ActionProjectType, ProjectMembershipRole, ProjectVersion, TableName } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import {
constructPermissionErrorMessage,
validatePrivilegeChangeOperation
} from "@app/ee/services/permission/permission-fns";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionMemberActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { TProjectUserAdditionalPrivilegeDALFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal"; import { TProjectUserAdditionalPrivilegeDALFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal";
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, ForbiddenRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors";
import { groupBy } from "@app/lib/fn"; import { groupBy } from "@app/lib/fn";
import { ms } from "@app/lib/ms"; import { ms } from "@app/lib/ms";
@@ -86,7 +89,7 @@ export const projectMembershipServiceFactory = ({
actorOrgId, actorOrgId,
actionProjectType: ActionProjectType.Any actionProjectType: ActionProjectType.Any
}); });
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Member); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Read, ProjectPermissionSub.Member);
const projectMembers = await projectMembershipDAL.findAllProjectMembers(projectId); const projectMembers = await projectMembershipDAL.findAllProjectMembers(projectId);
@@ -130,7 +133,7 @@ export const projectMembershipServiceFactory = ({
actorOrgId, actorOrgId,
actionProjectType: ActionProjectType.Any actionProjectType: ActionProjectType.Any
}); });
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Member); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Read, ProjectPermissionSub.Member);
const [membership] = await projectMembershipDAL.findAllProjectMembers(projectId, { username }); const [membership] = await projectMembershipDAL.findAllProjectMembers(projectId, { username });
if (!membership) throw new NotFoundError({ message: `Project membership not found for user '${username}'` }); if (!membership) throw new NotFoundError({ message: `Project membership not found for user '${username}'` });
@@ -153,7 +156,7 @@ export const projectMembershipServiceFactory = ({
actorOrgId, actorOrgId,
actionProjectType: ActionProjectType.Any actionProjectType: ActionProjectType.Any
}); });
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Member); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Read, ProjectPermissionSub.Member);
const [membership] = await projectMembershipDAL.findAllProjectMembers(projectId, { id }); const [membership] = await projectMembershipDAL.findAllProjectMembers(projectId, { id });
if (!membership) throw new NotFoundError({ message: `Project membership not found for user ${id}` }); if (!membership) throw new NotFoundError({ message: `Project membership not found for user ${id}` });
@@ -180,7 +183,7 @@ export const projectMembershipServiceFactory = ({
actorOrgId, actorOrgId,
actionProjectType: ActionProjectType.Any actionProjectType: ActionProjectType.Any
}); });
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Member); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Create, ProjectPermissionSub.Member);
const orgMembers = await orgDAL.findMembership({ const orgMembers = await orgDAL.findMembership({
[`${TableName.OrgMembership}.orgId` as "orgId"]: project.orgId, [`${TableName.OrgMembership}.orgId` as "orgId"]: project.orgId,
$in: { $in: {
@@ -253,7 +256,7 @@ export const projectMembershipServiceFactory = ({
membershipId, membershipId,
roles roles
}: TUpdateProjectMembershipDTO) => { }: TUpdateProjectMembershipDTO) => {
const { permission } = await permissionService.getProjectPermission({ const { permission, membership } = await permissionService.getProjectPermission({
actor, actor,
actorId, actorId,
projectId, projectId,
@@ -261,7 +264,7 @@ export const projectMembershipServiceFactory = ({
actorOrgId, actorOrgId,
actionProjectType: ActionProjectType.Any actionProjectType: ActionProjectType.Any
}); });
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Edit, ProjectPermissionSub.Member);
const membershipUser = await userDAL.findUserByProjectMembershipId(membershipId); const membershipUser = await userDAL.findUserByProjectMembershipId(membershipId);
if (membershipUser?.isGhost || membershipUser?.projectId !== projectId) { if (membershipUser?.isGhost || membershipUser?.projectId !== projectId) {
@@ -274,11 +277,21 @@ export const projectMembershipServiceFactory = ({
projectId projectId
); );
const permissionBoundary = validatePermissionBoundary(permission, rolePermission); const permissionBoundary = validatePrivilegeChangeOperation(
membership.shouldUseNewPrivilegeSystem,
ProjectPermissionMemberActions.GrantPrivileges,
ProjectPermissionSub.Member,
permission,
rolePermission
);
if (!permissionBoundary.isValid) if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({ throw new PermissionBoundaryError({
name: "PermissionBoundaryError", message: constructPermissionErrorMessage(
message: `Failed to change to a more privileged role ${requestedRoleChange}`, `Failed to change role ${requestedRoleChange}`,
membership.shouldUseNewPrivilegeSystem,
ProjectPermissionMemberActions.GrantPrivileges,
ProjectPermissionSub.Member
),
details: { missingPermissions: permissionBoundary.missingPermissions } details: { missingPermissions: permissionBoundary.missingPermissions }
}); });
} }
@@ -361,7 +374,7 @@ export const projectMembershipServiceFactory = ({
actorOrgId, actorOrgId,
actionProjectType: ActionProjectType.Any actionProjectType: ActionProjectType.Any
}); });
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Member); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Delete, ProjectPermissionSub.Member);
const member = await userDAL.findUserByProjectMembershipId(membershipId); const member = await userDAL.findUserByProjectMembershipId(membershipId);
@@ -397,7 +410,7 @@ export const projectMembershipServiceFactory = ({
actorOrgId, actorOrgId,
actionProjectType: ActionProjectType.Any actionProjectType: ActionProjectType.Any
}); });
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Member); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Delete, ProjectPermissionSub.Member);
const project = await projectDAL.findById(projectId); const project = await projectDAL.findById(projectId);
@@ -124,7 +124,7 @@ When `hostAPI` is not defined the operator fetches secrets from Infisical Cloud.
</Accordion> </Accordion>
<Accordion title="leaseTTL"> <Accordion title="leaseTTL">
The `leaseTTL` is a string-formatted duration that defines the time the lease should last for the dynamic secret. The `leaseTTL` is a string-formatted duration that defines the time the lease should last for the dynamic secret.
The format of the field is `[duration][unit]` where `duration` is a number and `unit` is a string representing the unit of time. The format of the field is `[duration][unit]` where `duration` is a number and `unit` is a string representing the unit of time.
-213
View File
@@ -1,213 +0,0 @@
---
title: "Permissions"
description: "Infisical's permissions system provides granular access control."
---
## Overview
The Infisical permissions system is based on a role-based access control (RBAC) model. The system allows you to define roles and assign them to users and machines. Each role has a set of permissions that define what actions a user can perform.
Permissions are built on a subject-action-object model. The subject is the resource the permission is being applied to, the action is what the permission allows.
An example of a subject/action combination would be `secrets/read`. This permission allows the subject to read secrets.
Refer to the table below for a list of subjects and the actions they support.
## Subjects and Actions
<Tabs>
<Tab title="Project Permissions">
<Note>
Not all actions are applicable to all subjects. As an example, the
`secrets-rollback` subject only supports `read`, and `create` as actions.
While `secrets` support `read`, `create`, `edit`, `delete`.
</Note>
| Subject | Actions |
| ------------------------- | ----------------------------------------------------------------------------------------------------------- |
| `role` | `read`, `create`, `edit`, `delete` |
| `member` | `read`, `create`, `edit`, `delete` |
| `groups` | `read`, `create`, `edit`, `delete` |
| `settings` | `read`, `create`, `edit`, `delete` |
| `integrations` | `read`, `create`, `edit`, `delete` |
| `webhooks` | `read`, `create`, `edit`, `delete` |
| `service-tokens` | `read`, `create`, `edit`, `delete` |
| `environments` | `read`, `create`, `edit`, `delete` |
| `tags` | `read`, `create`, `edit`, `delete` |
| `audit-logs` | `read`, `create`, `edit`, `delete` |
| `ip-allowlist` | `read`, `create`, `edit`, `delete` |
| `workspace` | `edit`, `delete` |
| `secrets` | `read`, `create`, `edit`, `delete` |
| `secret-folders` | `read`, `create`, `edit`, `delete` |
| `secret-imports` | `read`, `create`, `edit`, `delete` |
| `dynamic-secrets` | `read-root-credential`, `create-root-credential`, `edit-root-credential`, `delete-root-credential`, `lease` |
| `secret-rollback` | `read`, `create` |
| `secret-approval` | `read`, `create`, `edit`, `delete` |
| `secret-rotation` | `read`, `create`, `edit`, `delete` |
| `identity` | `read`, `create`, `edit`, `delete` |
| `certificate-authorities` | `read`, `create`, `edit`, `delete` |
| `certificates` | `read`, `create`, `edit`, `delete` |
| `certificate-templates` | `read`, `create`, `edit`, `delete` |
| `pki-alerts` | `read`, `create`, `edit`, `delete` |
| `pki-collections` | `read`, `create`, `edit`, `delete` |
| `kms` | `edit` |
| `cmek` | `read`, `create`, `edit`, `delete`, `encrypt`, `decrypt` |
| `secret-syncs` | `read`, `create`, `edit`, `delete`, `sync-secrets`, `import-secrets`, `remove-secrets` |
</Tab>
<Tab title="Organization Permissions">
<Note>
Not all actions are applicable to all subjects. As an example, the `workspace`
subject only supports `read`, and `create` as actions. While `member` support
`read`, `create`, `edit`, `delete`.
</Note>
| Subject | Actions |
| --------------------- | ------------------------------------------------ |
| `workspace` | `read`, `create` |
| `role` | `read`, `create`, `edit`, `delete` |
| `member` | `read`, `create`, `edit`, `delete` |
| `secret-scanning` | `read`, `create`, `edit`, `delete` |
| `settings` | `read`, `create`, `edit`, `delete` |
| `incident-account` | `read`, `create`, `edit`, `delete` |
| `sso` | `read`, `create`, `edit`, `delete` |
| `scim` | `read`, `create`, `edit`, `delete` |
| `ldap` | `read`, `create`, `edit`, `delete` |
| `groups` | `read`, `create`, `edit`, `delete` |
| `billing` | `read`, `create`, `edit`, `delete` |
| `identity` | `read`, `create`, `edit`, `delete` |
| `project-templates` | `read`, `create`, `edit`, `delete` |
| `app-connections` | `read`, `create`, `edit`, `delete`, `connect` |
| `kms` | `read` |
</Tab>
</Tabs>
## Inversion
Permission inversion allows you to explicitly deny actions instead of allowing them. This is supported for the following subjects:
- secrets
- secret-folders
- secret-imports
- dynamic-secrets
When a permission is inverted, it changes from an "allow" rule to a "deny" rule. For example:
```typescript
// Regular permission - allows reading secrets
{
subject: "secrets",
action: ["read"]
}
// Inverted permission - denies reading secrets
{
subject: "secrets",
action: ["read"],
inverted: true
}
```
## Conditions
Conditions allow you to create more granular permissions by specifying criteria that must be met for the permission to apply. This is supported for the following subjects:
- secrets
- secret-folders
- secret-imports
- dynamic-secrets
### Properties
Conditions can be applied to the following properties:
- `environment`: Control access based on environment slugs
- `secretPath`: Control access based on secret paths
- `secretName`: Control access based on secret names
- `secretTags`: Control access based on tags (only supports $in operator)
### Operators
The following operators are available for conditions:
| Operator | Description | Example |
| -------- | ---------------------------------- | ----------------------------------------------------- |
| `$eq` | Equal | `{ environment: { $eq: "production" } }` |
| `$ne` | Not equal | `{ environment: { $ne: "development" } }` |
| `$in` | Matches any value in array | `{ environment: { $in: ["staging", "production"] } }` |
| `$glob` | Pattern matching using glob syntax | `{ secretPath: { $glob: "/app/\*" } }` |
These details are especially useful if you're using the API to [create new project roles](../api-reference/endpoints/project-roles/create).
The rules outlined on this page, also apply when using our Terraform Provider to manage your Infisical project roles, or any other of our clients that manage project roles.
## Migrating from permission V1 to permission V2
When upgrading to V2 permissions (i.e. when moving from using the `permissions` to `permissions_v2` field in your Terraform configurations, or upgrading to the V2 permission API), you'll need to update your permission structure as follows:
Any permissions for `secrets` should be expanded to include equivalent permissions for:
- `secret-imports`
- `secret-folders` (except for read permissions)
- `dynamic-secrets`
For dynamic secrets, the actions need to be mapped differently:
- `read` → `read-root-credential`
- `create` → `create-root-credential`
- `edit` → `edit-root-credential` (also adds `lease` permission)
- `delete` → `delete-root-credential`
Example:
```hcl
# Old V1 configuration
resource "infisical_project_role" "example" {
name = "example"
permissions = [
{
subject = "secrets"
action = "read"
},
{
subject = "secrets"
action = "edit"
}
]
}
# New V2 configuration
resource "infisical_project_role" "example" {
name = "example"
permissions_v2 = [
# Original secrets permission
{
subject = "secrets"
action = ["read", "edit"]
inverted = false
},
# Add equivalent secret-imports permission
{
subject = "secret-imports"
action = ["read", "edit"]
inverted = false
},
# Add secret-folders permission (without read)
{
subject = "secret-folders"
action = ["edit"]
inverted = false
},
# Add dynamic-secrets permission with mapped actions
{
subject = "dynamic-secrets"
action = ["read-root-credential", "edit-root-credential", "lease"]
inverted = false
}
]
}
```
Note: When moving to V2 permissions, make sure to include all the necessary expanded permissions based on your original `secrets` permissions.
+118
View File
@@ -0,0 +1,118 @@
---
title: "Migration Guide"
description: "Guide for migrating permissions in Infisical"
---
# Migrating from Permission V1 to Permission V2
This guide provides instructions for upgrading from the legacy V1 permissions system to the more powerful V2 permissions system in Infisical.
## Why Upgrade to V2?
The V2 permissions system offers several advantages over V1:
- **More granular control**: Separate permissions for different secret-related resources
- **Explicit deny rules**: Support for permission inversion
- **Conditional permissions**: Apply permissions based on specific criteria
- **Array-based actions**: Cleaner syntax for multiple actions
## Migration Steps
When upgrading to V2 permissions (i.e., when moving from using the `permissions` to `permissions_v2` field in your Terraform configurations, or upgrading to the V2 permission API), you'll need to update your permission structure as follows:
### 1. Expand Secret Permissions
Any permissions for `secrets` should be expanded to include equivalent permissions for:
- `secret-imports`
- `secret-folders` (except for read permissions)
- `dynamic-secrets`
### 2. Map Dynamic Secret Actions
For dynamic secrets, the actions need to be mapped differently:
| V1 Action | V2 Action |
| --------- | ----------------------------------------------------- |
| `read` | `read-root-credential` |
| `create` | `create-root-credential` |
| `edit` | `edit-root-credential` (also adds `lease` permission) |
| `delete` | `delete-root-credential` |
### 3. Update Configuration Format
V2 permissions use a different syntax, with actions stored in arrays and an optional `inverted` flag:
```typescript
// V1 format (single action)
{
subject: "secrets",
action: "read"
}
// V2 format (array of actions)
{
subject: "secrets",
action: ["read"],
inverted: false // Optional, defaults to false
}
```
## Example Migration
Here's a complete example showing how to migrate a role from V1 to V2:
```hcl
# Old V1 configuration
resource "infisical_project_role" "example" {
name = "example"
permissions = [
{
subject = "secrets"
action = "read"
},
{
subject = "secrets"
action = "edit"
}
]
}
# New V2 configuration
resource "infisical_project_role" "example" {
name = "example"
permissions_v2 = [
# Original secrets permission
{
subject = "secrets"
action = ["read", "edit"]
inverted = false
},
# Add equivalent secret-imports permission
{
subject = "secret-imports"
action = ["read", "edit"]
inverted = false
},
# Add secret-folders permission (without read)
{
subject = "secret-folders"
action = ["edit"]
inverted = false
},
# Add dynamic-secrets permission with mapped actions
{
subject = "dynamic-secrets"
action = ["read-root-credential", "edit-root-credential", "lease"]
inverted = false
}
]
}
```
## Important Considerations
- When moving to V2 permissions, make sure to include all the necessary expanded permissions based on your original `secrets` permissions.
- V2 permissions give you the ability to use conditions and inversion, which are not available in V1.
- During migration, review your existing roles and consider if more granular permissions would better fit your security requirements.
- Test your migrated permissions thoroughly in a non-production environment before deploying to production.
@@ -0,0 +1,220 @@
---
title: "Organization Permissions"
description: "Comprehensive guide to Infisical's organization-level permissions"
---
## Overview
Infisical's organization permissions system follows a role-based access control (RBAC) model built on a subject-action-object framework. At the organization level, these permissions determine what actions users/machines can perform on various resources across the entire organization.
Each permission consists of:
- **Subject**: The resource the permission applies to (e.g., workspaces, members, billing)
- **Action**: The operation that can be performed (e.g., read, create, edit, delete)
Some organization-level resources—specifically `app-connections`—support conditional permissions and permission inversion for more granular access control.
## Available Organization Permissions
Below is a comprehensive list of all available organization-level subjects and their supported actions, organized by functional area.
### Workspace Management
#### Subject: `workspace`
| Action | Description |
| -------- | --------------------- |
| `create` | Create new workspaces |
### Role Management
#### Subject: `role`
| Action | Description |
| -------- | ------------------------------------------------------ |
| `read` | View organization roles and their assigned permissions |
| `create` | Create new organization roles |
| `edit` | Modify existing organization roles |
| `delete` | Remove organization roles |
### User Management
#### Subject: `member`
| Action | Description |
| -------- | ------------------------------------ |
| `read` | View organization members |
| `create` | Add new members to the organization |
| `edit` | Modify member details |
| `delete` | Remove members from the organization |
#### Subject: `groups`
| Action | Description |
| ------------------ | ------------------------------------------------ |
| `read` | View organization groups |
| `create` | Create new groups in the organization |
| `edit` | Modify existing groups |
| `delete` | Remove groups from the organization |
| `grant-privileges` | Change permission levels for organization groups |
| `add-members` | Add members to groups |
| `remove-members` | Remove members from groups |
#### Subject: `identity`
| Action | Description |
| ------------------ | --------------------------------------------------- |
| `read` | View organization identities |
| `create` | Add new identities to organization |
| `edit` | Modify organization identities |
| `delete` | Remove identities from organization |
| `grant-privileges` | Change permission levels of organization identities |
| `revoke-auth` | Revoke authentication for identities |
| `create-token` | Create new authentication tokens |
| `delete-token` | Delete authentication tokens |
| `get-token` | Retrieve authentication tokens |
### Security & Compliance
#### Subject: `secret-scanning`
| Action | Description |
| -------- | ----------------------------------------- |
| `read` | View secret scanning results and settings |
| `create` | Configure secret scanning |
| `edit` | Modify secret scanning settings |
| `delete` | Remove secret scanning configuration |
#### Subject: `settings`
| Action | Description |
| -------- | ----------------------------------------- |
| `read` | View organization settings |
| `create` | Setup and configure organization settings |
| `edit` | Modify organization settings |
| `delete` | Remove organization settings |
#### Subject: `incident-contact`
| Action | Description |
| -------- | -------------------------------- |
| `read` | View incident contacts |
| `create` | Set up new incident contacts |
| `edit` | Modify incident contact settings |
| `delete` | Remove incident contacts |
#### Subject: `audit-logs`
| Action | Description |
| ------ | ---------------------------- |
| `read` | View organization audit logs |
### Identity Provider Integration
#### Subject: `sso`
| Action | Description |
| -------- | ---------------------------------- |
| `read` | View Single Sign-On configurations |
| `create` | Set up new SSO integrations |
| `edit` | Modify existing SSO settings |
| `delete` | Remove SSO configurations |
#### Subject: `scim`
| Action | Description |
| -------- | ----------------------------- |
| `read` | View SCIM configurations |
| `create` | Set up new SCIM provisioning |
| `edit` | Modify existing SCIM settings |
| `delete` | Remove SCIM configurations |
#### Subject: `ldap`
| Action | Description |
| -------- | ----------------------------- |
| `read` | View LDAP configurations |
| `create` | Set up new LDAP integrations |
| `edit` | Modify existing LDAP settings |
| `delete` | Remove LDAP configurations |
### Billing & Subscriptions
#### Subject: `billing`
| Action | Description |
| -------- | ------------------------------------------------ |
| `read` | View billing information and subscription status |
| `create` | Set up new payment methods or subscriptions |
| `edit` | Modify billing details or subscription plans |
| `delete` | Remove payment methods or cancel subscriptions |
### Templates & Automation
#### Subject: `project-templates`
| Action | Description |
| -------- | --------------------------------- |
| `read` | View project templates |
| `create` | Create new project templates |
| `edit` | Modify existing project templates |
| `delete` | Remove project templates |
### Integrations
#### Subject: `app-connections`
Supports conditions and permission inversion
| Action | Description |
| --------- | ---------------------------------- |
| `read` | View app connection configurations |
| `create` | Create new app connections |
| `edit` | Modify existing app connections |
| `delete` | Remove app connections |
| `connect` | Use app connections |
### Key Management
#### Subject: `kms`
| Action | Description |
| -------- | ------------------------------------ |
| `read` | View organization KMS configurations |
| `create` | Set up new KMS configurations |
| `edit` | Modify KMS settings |
| `delete` | Remove KMS configurations |
#### Subject: `kmip`
| Action | Description |
| ------- | ---------------------------------- |
| `setup` | Configure KMIP server settings |
| `proxy` | Act as a proxy for KMIP operations |
### Admin Tools
#### Subject: `organization-admin-console`
| Action | Description |
| --------------------- | ------------------------------------------- |
| `access-all-projects` | Access all projects within the organization |
### Secure Share
#### Subject: `secret-share`
| Action | Description |
| ----------------- | ---------------------------- |
| `manage-settings` | Manage secret share settings |
### Gateway Management
#### Subject: `gateway`
| Action | Description |
| ----------------- | --------------------------------- |
| `list-gateways` | View all organization gateways |
| `create-gateways` | Add new gateways to organization |
| `edit-gateways` | Modify existing gateway settings |
| `delete-gateways` | Remove gateways from organization |
+89
View File
@@ -0,0 +1,89 @@
---
title: "Overview"
description: "Infisical's permissions system provides granular access control."
---
## Overview
The Infisical permissions system is based on a role-based access control (RBAC) model. The system allows you to define roles and assign them to users and machines. Each role has a set of permissions that define what actions a user can perform.
Permissions are built on a subject-action-object model. The subject is the resource the permission is being applied to, the action is what the permission allows.
An example of a subject/action combination would be `secrets/read`. This permission allows the subject to read secrets.
## Permission Scope Levels
Infisical's permission system operates at two distinct levels, providing comprehensive and flexible access control across your entire security infrastructure:
### Project Permissions
Project permissions control access to resources within a specific project, including secrets management, PKI, KMS, and SSH certificate functionality.
For a comprehensive list of all project-level subjects, actions, and detailed descriptions, please refer to the [Project Permissions](/internals/permissions/project-permissions) documentation.
### Organization Permissions
Organization permissions control access to organization-wide resources and settings such as workspaces, billing, identity providers, and more.
For a comprehensive list of all organization-level subjects, actions, and detailed descriptions, please refer to the [Organization Permissions](/internals/permissions/organization-permissions) documentation.
## Inversion
Permission inversion allows you to explicitly deny actions instead of allowing them. This is supported for the following subjects:
- secrets
- secret-folders
- secret-imports
- dynamic-secrets
When a permission is inverted, it changes from an "allow" rule to a "deny" rule. For example:
```typescript
// Regular permission - allows reading secrets
{
subject: "secrets",
action: ["read"]
}
// Inverted permission - denies reading secrets
{
subject: "secrets",
action: ["read"],
inverted: true
}
```
**Important:** The order of permissions matters when using inversion. For inverted (deny) permissions to be effective, there
typically needs to be a corresponding allow permission somewhere in the chain. Permissions are evaluated in sequence,
so the relative positioning of allow and deny rules determines the final access outcome.
## Conditions
Conditions allow you to create more granular permissions by specifying criteria that must be met for the permission to apply. This is supported for the following subjects:
- secrets
- secret-folders
- secret-imports
- dynamic-secrets
### Properties
Conditions can be applied to the following properties:
- `environment`: Control access based on environment slugs
- `secretPath`: Control access based on secret paths
- `secretName`: Control access based on secret names
- `secretTags`: Control access based on tags (only supports $in operator)
### Operators
The following operators are available for conditions:
| Operator | Description | Example |
| -------- | ---------------------------------- | ----------------------------------------------------- |
| `$eq` | Equal | `{ environment: { $eq: "production" } }` |
| `$ne` | Not equal | `{ environment: { $ne: "development" } }` |
| `$in` | Matches any value in array | `{ environment: { $in: ["staging", "production"] } }` |
| `$glob` | Pattern matching using glob syntax | `{ secretPath: { $glob: "/app/\*" } }` |
These details are especially useful if you're using the API to [create new project roles](../api-reference/endpoints/project-roles/create).
The rules outlined on this page, also apply when using our Terraform Provider to manage your Infisical project roles, or any other of our clients that manage project roles.
@@ -0,0 +1,310 @@
---
title: "Project Permissions"
description: "Comprehensive guide to Infisical's project-level permissions"
---
## Overview
Infisical's project permissions system follows a role-based access control (RBAC) model built on a subject-action-object framework. At the project level, these permissions determine what actions users/machines can perform on various resources within a specific project.
Each permission consists of:
- **Subject**: The resource the permission applies to (e.g., secrets, members, settings)
- **Action**: The operation that can be performed (e.g., read, create, edit, delete)
Some project-level resources—specifically `secrets`, `secret-folders`, `secret-imports`, and `dynamic-secrets`—support conditional permissions and permission inversion for more granular access control. Conditions allow you to specify criteria (like environment, secret path, or tags) that must be met for the permission to apply.
## Available Project Permissions
Below is a comprehensive list of all available project-level subjects and their supported actions.
### Core Platform & Access Control
#### Subject: `role`
| Action | Description |
| -------- | ------------------------------------------------- |
| `read` | View project roles and their assigned permissions |
| `create` | Create new project roles |
| `edit` | Modify existing project roles |
| `delete` | Remove project roles |
#### Subject: `member`
| Action | Description |
| ------------------ | ------------------------------------------- |
| `read` | View project members |
| `create` | Add new members to the project |
| `edit` | Modify member details |
| `delete` | Remove members from the project |
| `grant-privileges` | Change permission levels of project members |
#### Subject: `groups`
| Action | Description |
| ------------------ | ------------------------------------------ |
| `read` | View project groups |
| `create` | Create new groups within the project |
| `edit` | Modify existing groups |
| `delete` | Remove groups from the project |
| `grant-privileges` | Change permission levels of project groups |
#### Subject: `identity`
| Action | Description |
| ------------------ | ---------------------------------------------- |
| `read` | View project identities |
| `create` | Add new identities to project |
| `edit` | Modify project identities |
| `delete` | Remove identities from project |
| `grant-privileges` | Change permission levels of project identities |
#### Subject: `settings`
| Action | Description |
| -------- | -------------------------------------- |
| `read` | View project settings |
| `create` | Add new project configuration settings |
| `edit` | Modify project settings |
| `delete` | Remove project settings |
#### Subject: `environments`
| Action | Description |
| -------- | ------------------------------------ |
| `read` | View project environments |
| `create` | Add new environments to the project |
| `edit` | Modify existing environments |
| `delete` | Remove environments from the project |
#### Subject: `tags`
| Action | Description |
| -------- | ---------------------------------------- |
| `read` | View project tags |
| `create` | Create new tags for organizing resources |
| `edit` | Modify existing tags |
| `delete` | Remove tags from the project |
#### Subject: `workspace`
| Action | Description |
| -------- | ------------------------- |
| `edit` | Modify workspace settings |
| `delete` | Delete the workspace |
#### Subject: `ip-allowlist`
| Action | Description |
| -------- | -------------------------------------------- |
| `read` | View IP allowlists |
| `create` | Add new IP addresses or ranges to allowlists |
| `edit` | Modify existing IP allowlist entries |
| `delete` | Remove IP addresses from allowlists |
#### Subject: `audit-logs`
| Action | Description |
| ------ | ------------------------------------------------------- |
| `read` | View audit logs of actions performed within the project |
#### Subject: `integrations`
| Action | Description |
| -------- | -------------------------------- |
| `read` | View configured integrations |
| `create` | Add new third-party integrations |
| `edit` | Modify integration settings |
| `delete` | Remove integrations |
#### Subject: `webhooks`
| Action | Description |
| -------- | ------------------------------------ |
| `read` | View webhook configurations |
| `create` | Add new webhooks |
| `edit` | Modify webhook endpoints or triggers |
| `delete` | Remove webhooks |
#### Subject: `service-tokens`
| Action | Description |
| -------- | ---------------------------------------- |
| `read` | View service tokens |
| `create` | Create new service tokens for API access |
| `edit` | Modify token properties |
| `delete` | Revoke or remove service tokens |
### Secrets Management
#### Subject: `secrets`
Supports conditions and permission inversion
| Action | Description |
| -------- | ------------------------------- |
| `read` | View secrets and their values |
| `create` | Add new secrets to the project |
| `edit` | Modify existing secret values |
| `delete` | Remove secrets from the project |
#### Subject: `secret-folders`
Supports conditions and permission inversion
| Action | Description |
| -------- | ------------------------ |
| `read` | View secret folders |
| `create` | Create new folders |
| `edit` | Modify folder properties |
| `delete` | Remove secret folders |
#### Subject: `secret-imports`
Supports conditions and permission inversion
| Action | Description |
| -------- | --------------------- |
| `read` | View secret imports |
| `create` | Create secret imports |
| `edit` | Modify secret imports |
| `delete` | Remove secret imports |
#### Subject: `secret-rollback`
| Action | Description |
| -------- | ---------------------------------- |
| `read` | View secret versions and snapshots |
| `create` | Roll back secrets to snapshots |
#### Subject: `secret-approval`
| Action | Description |
| -------- | ----------------------------------- |
| `read` | View approval policies and requests |
| `create` | Create new approval policies |
| `edit` | Modify approval policies |
| `delete` | Remove approval policies |
#### Subject: `secret-rotation`
| Action | Description |
| -------- | ------------------------------------- |
| `read` | View secret rotation policies |
| `create` | Set up automatic secret rotation |
| `edit` | Modify rotation schedules or policies |
| `delete` | Remove rotation policies |
#### Subject: `secret-syncs`
| Action | Description |
| ---------------- | -------------------------------------------------- |
| `read` | View secret synchronization configurations |
| `create` | Create new sync configurations |
| `edit` | Modify existing sync settings |
| `delete` | Remove sync configurations |
| `sync-secrets` | Execute synchronization of secrets between systems |
| `import-secrets` | Import secrets from sync sources |
| `remove-secrets` | Remove secrets from sync destinations |
#### Subject: `dynamic-secrets`
Supports conditions and permission inversion
| Action | Description |
| ------------------------ | ---------------------------------- |
| `read-root-credential` | View dynamic secret configurations |
| `create-root-credential` | Create dynamic secrets |
| `edit-root-credential` | Edit dynamic secrets |
| `delete-root-credential` | Remove dynamic secrets |
| `lease` | Create dynamic secret leases |
### Key Management Service (KMS)
#### Subject: `kms`
| Action | Description |
| ------ | --------------------------- |
| `edit` | Modify project KMS settings |
#### Subject: `cmek`
| Action | Description |
| --------- | ------------------------------------- |
| `read` | View Customer-Managed Encryption Keys |
| `create` | Add new encryption keys |
| `edit` | Modify key properties |
| `delete` | Remove encryption keys |
| `encrypt` | Use keys for encryption operations |
| `decrypt` | Use keys for decryption operations |
### Public Key Infrastructure (PKI)
#### Subject: `certificate-authorities`
| Action | Description |
| -------- | ---------------------------------- |
| `read` | View certificate authorities |
| `create` | Create new certificate authorities |
| `edit` | Modify CA configurations |
| `delete` | Remove certificate authorities |
#### Subject: `certificates`
| Action | Description |
| -------- | ----------------------------- |
| `read` | View certificates |
| `create` | Issue new certificates |
| `delete` | Revoke or remove certificates |
#### Subject: `certificate-templates`
| Action | Description |
| -------- | -------------------------------- |
| `read` | View certificate templates |
| `create` | Create new certificate templates |
| `edit` | Modify template configurations |
| `delete` | Remove certificate templates |
#### Subject: `pki-alerts`
| Action | Description |
| -------- | ------------------------------------------------------------ |
| `read` | View PKI alert configurations |
| `create` | Create new alerts for certificate expiry or other PKI events |
| `edit` | Modify alert settings |
| `delete` | Remove PKI alerts |
#### Subject: `pki-collections`
| Action | Description |
| -------- | --------------------------------------------------- |
| `read` | View PKI resource collections |
| `create` | Create new collections for organizing PKI resources |
| `edit` | Modify collection properties |
| `delete` | Remove PKI collections |
### SSH Certificate Management
#### Subject: `ssh-certificate-authorities`
| Action | Description |
| -------- | -------------------------------------- |
| `read` | View SSH certificate authorities |
| `create` | Create new SSH certificate authorities |
| `edit` | Modify SSH CA configurations |
| `delete` | Remove SSH certificate authorities |
#### Subject: `ssh-certificates`
| Action | Description |
| -------- | --------------------------------- |
| `read` | View SSH certificates |
| `create` | Issue new SSH certificates |
| `edit` | Modify SSH certificate properties |
| `delete` | Revoke or remove SSH certificates |
#### Subject: `ssh-certificate-templates`
| Action | Description |
| -------- | ------------------------------------ |
| `read` | View SSH certificate templates |
| `create` | Create new SSH certificate templates |
| `edit` | Modify SSH template configurations |
| `delete` | Remove SSH certificate templates |
+9 -1
View File
@@ -1187,7 +1187,15 @@
"group": "Internals", "group": "Internals",
"pages": [ "pages": [
"internals/overview", "internals/overview",
"internals/permissions", {
"group": "Permissions",
"pages": [
"internals/permissions/overview",
"internals/permissions/project-permissions",
"internals/permissions/organization-permissions",
"internals/permissions/migration"
]
},
"internals/components", "internals/components",
"internals/security", "internals/security",
"internals/service-tokens" "internals/service-tokens"
@@ -17,6 +17,8 @@ export type CheckboxProps = Omit<
isError?: boolean; isError?: boolean;
isIndeterminate?: boolean; isIndeterminate?: boolean;
containerClassName?: string; containerClassName?: string;
indicatorClassName?: string;
allowMultilineLabel?: boolean;
}; };
export const Checkbox = ({ export const Checkbox = ({
@@ -30,6 +32,8 @@ export const Checkbox = ({
isError, isError,
isIndeterminate, isIndeterminate,
containerClassName, containerClassName,
indicatorClassName,
allowMultilineLabel,
...props ...props
}: CheckboxProps): JSX.Element => { }: CheckboxProps): JSX.Element => {
return ( return (
@@ -48,7 +52,9 @@ export const Checkbox = ({
{...props} {...props}
id={id} id={id}
> >
<CheckboxPrimitive.Indicator className={`${checkIndicatorBg || "text-bunker-800"}`}> <CheckboxPrimitive.Indicator
className={twMerge(`${checkIndicatorBg || "text-bunker-800"}`, indicatorClassName)}
>
{isIndeterminate ? ( {isIndeterminate ? (
<FontAwesomeIcon icon={faMinus} size="sm" /> <FontAwesomeIcon icon={faMinus} size="sm" />
) : ( ) : (
@@ -57,7 +63,11 @@ export const Checkbox = ({
</CheckboxPrimitive.Indicator> </CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root> </CheckboxPrimitive.Root>
<label <label
className={twMerge("truncate whitespace-nowrap text-sm", isError && "text-red-400")} className={twMerge(
"text-sm",
!allowMultilineLabel && "truncate whitespace-nowrap",
isError && "text-red-400"
)}
htmlFor={id} htmlFor={id}
> >
{children} {children}
@@ -1,3 +1,8 @@
export { useOrgPermission } from "./OrgPermissionContext"; export { useOrgPermission } from "./OrgPermissionContext";
export type { TOrgPermission } from "./types"; export type { TOrgPermission } from "./types";
export { OrgPermissionActions, OrgPermissionSubjects } from "./types"; export {
OrgPermissionActions,
OrgPermissionGroupActions,
OrgPermissionIdentityActions,
OrgPermissionSubjects
} from "./types";
@@ -59,6 +59,28 @@ export enum OrgPermissionKmipActions {
Setup = "setup" Setup = "setup"
} }
export enum OrgPermissionIdentityActions {
Read = "read",
Create = "create",
Edit = "edit",
Delete = "delete",
GrantPrivileges = "grant-privileges",
RevokeAuth = "revoke-auth",
CreateToken = "create-token",
GetToken = "get-token",
DeleteToken = "delete-token"
}
export enum OrgPermissionGroupActions {
Read = "read",
Create = "create",
Edit = "edit",
Delete = "delete",
GrantPrivileges = "grant-privileges",
AddMembers = "add-members",
RemoveMembers = "remove-members"
}
export type AppConnectionSubjectFields = { export type AppConnectionSubjectFields = {
connectionId: string; connectionId: string;
}; };
@@ -73,15 +95,15 @@ export type OrgPermissionSet =
| [OrgPermissionActions, OrgPermissionSubjects.Scim] | [OrgPermissionActions, OrgPermissionSubjects.Scim]
| [OrgPermissionActions, OrgPermissionSubjects.Sso] | [OrgPermissionActions, OrgPermissionSubjects.Sso]
| [OrgPermissionActions, OrgPermissionSubjects.Ldap] | [OrgPermissionActions, OrgPermissionSubjects.Ldap]
| [OrgPermissionActions, OrgPermissionSubjects.Groups] | [OrgPermissionGroupActions, OrgPermissionSubjects.Groups]
| [OrgPermissionActions, OrgPermissionSubjects.SecretScanning] | [OrgPermissionActions, OrgPermissionSubjects.SecretScanning]
| [OrgPermissionActions, OrgPermissionSubjects.Billing] | [OrgPermissionActions, OrgPermissionSubjects.Billing]
| [OrgPermissionActions, OrgPermissionSubjects.Identity]
| [OrgPermissionActions, OrgPermissionSubjects.Kms] | [OrgPermissionActions, OrgPermissionSubjects.Kms]
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole] | [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole]
| [OrgPermissionActions, OrgPermissionSubjects.AuditLogs] | [OrgPermissionActions, OrgPermissionSubjects.AuditLogs]
| [OrgPermissionActions, OrgPermissionSubjects.ProjectTemplates] | [OrgPermissionActions, OrgPermissionSubjects.ProjectTemplates]
| [OrgPermissionAppConnectionActions, OrgPermissionSubjects.AppConnections] | [OrgPermissionAppConnectionActions, OrgPermissionSubjects.AppConnections]
| [OrgPermissionIdentityActions, OrgPermissionSubjects.Identity]
| [OrgPermissionKmipActions, OrgPermissionSubjects.Kmip] | [OrgPermissionKmipActions, OrgPermissionSubjects.Kmip]
| [OrgGatewayPermissionActions, OrgPermissionSubjects.Gateway] | [OrgGatewayPermissionActions, OrgPermissionSubjects.Gateway]
| [OrgPermissionSecretShareAction, OrgPermissionSubjects.SecretShare]; | [OrgPermissionSecretShareAction, OrgPermissionSubjects.SecretShare];
@@ -4,6 +4,9 @@ export {
ProjectPermissionActions, ProjectPermissionActions,
ProjectPermissionCmekActions, ProjectPermissionCmekActions,
ProjectPermissionDynamicSecretActions, ProjectPermissionDynamicSecretActions,
ProjectPermissionGroupActions,
ProjectPermissionIdentityActions,
ProjectPermissionKmipActions, ProjectPermissionKmipActions,
ProjectPermissionMemberActions,
ProjectPermissionSub ProjectPermissionSub
} from "./types"; } from "./types";
@@ -51,6 +51,30 @@ export enum ProjectPermissionSecretSyncActions {
RemoveSecrets = "remove-secrets" RemoveSecrets = "remove-secrets"
} }
export enum ProjectPermissionIdentityActions {
Read = "read",
Create = "create",
Edit = "edit",
Delete = "delete",
GrantPrivileges = "grant-privileges"
}
export enum ProjectPermissionMemberActions {
Read = "read",
Create = "create",
Edit = "edit",
Delete = "delete",
GrantPrivileges = "grant-privileges"
}
export enum ProjectPermissionGroupActions {
Read = "read",
Create = "create",
Edit = "edit",
Delete = "delete",
GrantPrivileges = "grant-privileges"
}
export enum PermissionConditionOperators { export enum PermissionConditionOperators {
$IN = "$in", $IN = "$in",
$ALL = "$all", $ALL = "$all",
+5
View File
@@ -2,6 +2,8 @@ export { useOrganization } from "./OrganizationContext";
export type { TOrgPermission } from "./OrgPermissionContext"; export type { TOrgPermission } from "./OrgPermissionContext";
export { export {
OrgPermissionActions, OrgPermissionActions,
OrgPermissionGroupActions,
OrgPermissionIdentityActions,
OrgPermissionSubjects, OrgPermissionSubjects,
useOrgPermission useOrgPermission
} from "./OrgPermissionContext"; } from "./OrgPermissionContext";
@@ -10,7 +12,10 @@ export {
ProjectPermissionActions, ProjectPermissionActions,
ProjectPermissionCmekActions, ProjectPermissionCmekActions,
ProjectPermissionDynamicSecretActions, ProjectPermissionDynamicSecretActions,
ProjectPermissionGroupActions,
ProjectPermissionIdentityActions,
ProjectPermissionKmipActions, ProjectPermissionKmipActions,
ProjectPermissionMemberActions,
ProjectPermissionSub, ProjectPermissionSub,
useProjectPermission useProjectPermission
} from "./ProjectPermissionContext"; } from "./ProjectPermissionContext";
+2 -1
View File
@@ -20,5 +20,6 @@ export {
useGetOrgTaxIds, useGetOrgTaxIds,
useGetOrgTrialUrl, useGetOrgTrialUrl,
useUpdateOrg, useUpdateOrg,
useUpdateOrgBillingDetails useUpdateOrgBillingDetails,
useUpgradePrivilegeSystem
} from "./queries"; } from "./queries";
@@ -129,6 +129,18 @@ export const useUpdateOrg = () => {
}); });
}; };
export const useUpgradePrivilegeSystem = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: () => {
return apiRequest.post("/api/v2/organizations/privilege-system-upgrade");
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: organizationKeys.getUserOrganizations });
}
});
};
export const useGetOrgTrialUrl = () => { export const useGetOrgTrialUrl = () => {
return useMutation({ return useMutation({
mutationFn: async ({ orgId, success_url }: { orgId: string; success_url: string }) => { mutationFn: async ({ orgId, success_url }: { orgId: string; success_url: string }) => {
@@ -15,6 +15,7 @@ export type Organization = {
defaultMembershipRole: string; defaultMembershipRole: string;
enforceMfa: boolean; enforceMfa: boolean;
selectedMfaMethod?: MfaMethod; selectedMfaMethod?: MfaMethod;
shouldUseNewPrivilegeSystem: boolean;
allowSecretSharingOutsideOrganization?: boolean; allowSecretSharingOutsideOrganization?: boolean;
}; };
@@ -1,17 +1,30 @@
import { useState } from "react";
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate, useSearch } from "@tanstack/react-router"; import { useNavigate, useSearch } from "@tanstack/react-router";
import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; import { Button, PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes"; import { ROUTE_PATHS } from "@app/const/routes";
import { OrgPermissionActions, OrgPermissionSubjects, useOrgPermission } from "@app/context"; import {
OrgPermissionActions,
OrgPermissionGroupActions,
OrgPermissionIdentityActions,
OrgPermissionSubjects,
useOrganization,
useOrgPermission
} from "@app/context";
import { OrgAccessControlTabSections } from "@app/types/org"; import { OrgAccessControlTabSections } from "@app/types/org";
import { UpgradePrivilegeSystemModal } from "./components/UpgradePrivilegeSystemModal/UpgradePrivilegeSystemModal";
import { OrgGroupsTab, OrgIdentityTab, OrgMembersTab, OrgRoleTabSection } from "./components"; import { OrgGroupsTab, OrgIdentityTab, OrgMembersTab, OrgRoleTabSection } from "./components";
export const AccessManagementPage = () => { export const AccessManagementPage = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { permission } = useOrgPermission(); const { permission } = useOrgPermission();
const { currentOrg } = useOrganization();
const navigate = useNavigate({ const navigate = useNavigate({
from: ROUTE_PATHS.Organization.AccessControlPage.path from: ROUTE_PATHS.Organization.AccessControlPage.path
}); });
@@ -21,6 +34,8 @@ export const AccessManagementPage = () => {
structuralSharing: true structuralSharing: true
}); });
const [isUpgradePrivilegeSystemModalOpen, setIsUpgradePrivilegeSystemModalOpen] = useState(false);
const updateSelectedTab = (tab: string) => { const updateSelectedTab = (tab: string) => {
navigate({ navigate({
search: { selectedTab: tab } search: { selectedTab: tab }
@@ -37,13 +52,16 @@ export const AccessManagementPage = () => {
{ {
key: OrgAccessControlTabSections.Groups, key: OrgAccessControlTabSections.Groups,
label: "Groups", label: "Groups",
isHidden: permission.cannot(OrgPermissionActions.Read, OrgPermissionSubjects.Groups), isHidden: permission.cannot(OrgPermissionGroupActions.Read, OrgPermissionSubjects.Groups),
component: OrgGroupsTab component: OrgGroupsTab
}, },
{ {
key: OrgAccessControlTabSections.Identities, key: OrgAccessControlTabSections.Identities,
label: "Identities", label: "Identities",
isHidden: permission.cannot(OrgPermissionActions.Read, OrgPermissionSubjects.Identity), isHidden: permission.cannot(
OrgPermissionIdentityActions.Read,
OrgPermissionSubjects.Identity
),
component: OrgIdentityTab component: OrgIdentityTab
}, },
{ {
@@ -64,6 +82,30 @@ export const AccessManagementPage = () => {
title="Organization Access Control" title="Organization Access Control"
description="Manage fine-grained access for users, groups, roles, and identities within your organization resources." description="Manage fine-grained access for users, groups, roles, and identities within your organization resources."
/> />
{!currentOrg.shouldUseNewPrivilegeSystem && (
<div className="mb-4 mt-4 flex flex-col rounded-r border-l-2 border-l-primary bg-mineshaft-300/5 px-4 py-2.5">
<div className="mb-1 flex items-center text-sm">
<FontAwesomeIcon icon={faInfoCircle} size="sm" className="mr-1.5 text-primary" />
Your organization is using legacy privilege management
</div>
<p className="mb-2 mt-1 text-sm text-bunker-300">
We&apos;ve developed an improved privilege management system to better serve your
security needs. Upgrade to our new permission-based approach that allows you to
explicitly designate who can modify specific access levels, rather than relying on hierarchy comparisons.
</p>
<Button
colorSchema="primary"
className="mt-2 w-fit text-xs"
onClick={() => setIsUpgradePrivilegeSystemModalOpen(true)}
>
Learn More & Upgrade
</Button>
</div>
)}
<UpgradePrivilegeSystemModal
isOpen={isUpgradePrivilegeSystemModalOpen}
onOpenChange={setIsUpgradePrivilegeSystemModalOpen}
/>
<Tabs value={selectedTab} onValueChange={updateSelectedTab}> <Tabs value={selectedTab} onValueChange={updateSelectedTab}>
<TabList> <TabList>
{tabSections {tabSections
@@ -5,7 +5,7 @@ import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions"; import { OrgPermissionCan } from "@app/components/permissions";
import { Button, DeleteActionModal } from "@app/components/v2"; import { Button, DeleteActionModal } from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects, useSubscription } from "@app/context"; import { OrgPermissionGroupActions, OrgPermissionSubjects, useSubscription } from "@app/context";
import { useDeleteGroup } from "@app/hooks/api"; import { useDeleteGroup } from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp"; import { usePopUp } from "@app/hooks/usePopUp";
@@ -58,7 +58,7 @@ export const OrgGroupsSection = () => {
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"> <div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex justify-between"> <div className="mb-4 flex justify-between">
<p className="text-xl font-semibold text-mineshaft-100">Groups</p> <p className="text-xl font-semibold text-mineshaft-100">Groups</p>
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Groups}> <OrgPermissionCan I={OrgPermissionGroupActions.Create} a={OrgPermissionSubjects.Groups}>
{(isAllowed) => ( {(isAllowed) => (
<Button <Button
colorSchema="primary" colorSchema="primary"
@@ -33,7 +33,7 @@ import {
THead, THead,
Tr Tr
} from "@app/components/v2"; } from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context"; import { OrgPermissionGroupActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { usePagination, useResetPageHelper } from "@app/hooks"; import { usePagination, useResetPageHelper } from "@app/hooks";
import { useGetOrganizationGroups, useGetOrgRoles, useUpdateGroup } from "@app/hooks/api"; import { useGetOrganizationGroups, useGetOrgRoles, useUpdateGroup } from "@app/hooks/api";
import { OrderByDirection } from "@app/hooks/api/generic/types"; import { OrderByDirection } from "@app/hooks/api/generic/types";
@@ -241,7 +241,7 @@ export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => {
<Td>{slug}</Td> <Td>{slug}</Td>
<Td> <Td>
<OrgPermissionCan <OrgPermissionCan
I={OrgPermissionActions.Edit} I={OrgPermissionGroupActions.Edit}
a={OrgPermissionSubjects.Groups} a={OrgPermissionSubjects.Groups}
> >
{(isAllowed) => { {(isAllowed) => {
@@ -289,7 +289,7 @@ export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => {
Copy Group ID Copy Group ID
</DropdownMenuItem> </DropdownMenuItem>
<OrgPermissionCan <OrgPermissionCan
I={OrgPermissionActions.Edit} I={OrgPermissionGroupActions.Edit}
a={OrgPermissionSubjects.Groups} a={OrgPermissionSubjects.Groups}
> >
{(isAllowed) => ( {(isAllowed) => (
@@ -315,7 +315,7 @@ export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => {
)} )}
</OrgPermissionCan> </OrgPermissionCan>
<OrgPermissionCan <OrgPermissionCan
I={OrgPermissionActions.Edit} I={OrgPermissionGroupActions.Edit}
a={OrgPermissionSubjects.Groups} a={OrgPermissionSubjects.Groups}
> >
{(isAllowed) => ( {(isAllowed) => (
@@ -339,7 +339,7 @@ export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => {
)} )}
</OrgPermissionCan> </OrgPermissionCan>
<OrgPermissionCan <OrgPermissionCan
I={OrgPermissionActions.Delete} I={OrgPermissionGroupActions.Delete}
a={OrgPermissionSubjects.Groups} a={OrgPermissionSubjects.Groups}
> >
{(isAllowed) => ( {(isAllowed) => (
@@ -6,7 +6,7 @@ import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions"; import { OrgPermissionCan } from "@app/components/permissions";
import { Button, DeleteActionModal } from "@app/components/v2"; import { Button, DeleteActionModal } from "@app/components/v2";
import { import {
OrgPermissionActions, OrgPermissionIdentityActions,
OrgPermissionSubjects, OrgPermissionSubjects,
useOrganization, useOrganization,
useSubscription useSubscription
@@ -87,7 +87,10 @@ export const IdentitySection = withPermission(
/> />
</a> </a>
</div> </div>
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Identity}> <OrgPermissionCan
I={OrgPermissionIdentityActions.Create}
a={OrgPermissionSubjects.Identity}
>
{(isAllowed) => ( {(isAllowed) => (
<Button <Button
colorSchema="primary" colorSchema="primary"
@@ -143,5 +146,5 @@ export const IdentitySection = withPermission(
</div> </div>
); );
}, },
{ action: OrgPermissionActions.Read, subject: OrgPermissionSubjects.Identity } { action: OrgPermissionIdentityActions.Read, subject: OrgPermissionSubjects.Identity }
); );
@@ -32,7 +32,7 @@ import {
THead, THead,
Tr Tr
} from "@app/components/v2"; } from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context"; import { OrgPermissionIdentityActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { usePagination, useResetPageHelper } from "@app/hooks"; import { usePagination, useResetPageHelper } from "@app/hooks";
import { useGetIdentityMembershipOrgs, useGetOrgRoles, useUpdateIdentity } from "@app/hooks/api"; import { useGetIdentityMembershipOrgs, useGetOrgRoles, useUpdateIdentity } from "@app/hooks/api";
import { OrderByDirection } from "@app/hooks/api/generic/types"; import { OrderByDirection } from "@app/hooks/api/generic/types";
@@ -207,7 +207,7 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
<Td>{name}</Td> <Td>{name}</Td>
<Td> <Td>
<OrgPermissionCan <OrgPermissionCan
I={OrgPermissionActions.Edit} I={OrgPermissionIdentityActions.Edit}
a={OrgPermissionSubjects.Identity} a={OrgPermissionSubjects.Identity}
> >
{(isAllowed) => { {(isAllowed) => {
@@ -243,7 +243,7 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1"> <DropdownMenuContent align="start" className="p-1">
<OrgPermissionCan <OrgPermissionCan
I={OrgPermissionActions.Edit} I={OrgPermissionIdentityActions.Edit}
a={OrgPermissionSubjects.Identity} a={OrgPermissionSubjects.Identity}
> >
{(isAllowed) => ( {(isAllowed) => (
@@ -267,7 +267,7 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
)} )}
</OrgPermissionCan> </OrgPermissionCan>
<OrgPermissionCan <OrgPermissionCan
I={OrgPermissionActions.Delete} I={OrgPermissionIdentityActions.Delete}
a={OrgPermissionSubjects.Identity} a={OrgPermissionSubjects.Identity}
> >
{(isAllowed) => ( {(isAllowed) => (
@@ -0,0 +1,293 @@
import { useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { Button, Checkbox, Modal, ModalContent } from "@app/components/v2";
import { useOrgPermission } from "@app/context";
import { useUpgradePrivilegeSystem } from "@app/hooks/api";
const formSchema = z.object({
isProjectPrivilegesUpdated: z.literal(true),
isOrgPrivilegesUpdated: z.literal(true),
isInfrastructureUpdated: z.literal(true),
acknowledgesPermanentChange: z.literal(true)
});
type Props = {
isOpen?: boolean;
onOpenChange: (isOpen: boolean) => void;
};
export const UpgradePrivilegeSystemModal = ({ isOpen, onOpenChange }: Props) => {
const { membership } = useOrgPermission();
const [step, setStep] = useState<"info" | "upgrade">("info");
const {
handleSubmit,
control,
watch,
formState: { isSubmitting }
} = useForm({ resolver: zodResolver(formSchema) });
const { mutateAsync: upgradePrivilegeSystem } = useUpgradePrivilegeSystem();
const isProjectPrivilegesUpdated = watch("isProjectPrivilegesUpdated");
const isOrgPrivilegesUpdated = watch("isOrgPrivilegesUpdated");
const isInfrastructureUpdated = watch("isInfrastructureUpdated");
const acknowledgesPermanentChange = watch("acknowledgesPermanentChange");
const isAllChecksCompleted =
isProjectPrivilegesUpdated &&
isOrgPrivilegesUpdated &&
isInfrastructureUpdated &&
acknowledgesPermanentChange;
const handlePrivilegeSystemUpgrade = async () => {
try {
await upgradePrivilegeSystem();
createNotification({
text: "Privilege system upgrade completed",
type: "success"
});
onOpenChange(false);
} catch {
createNotification({
text: "Failed to upgrade privilege system",
type: "error"
});
}
};
const handleClose = () => {
onOpenChange(false);
setStep("info");
};
const isAdmin = membership?.role === "admin";
return (
<Modal isOpen={isOpen} onOpenChange={handleClose}>
<ModalContent title="Privilege Management System Upgrade" className="max-w-2xl">
{step === "info" ? (
<div className="mb-4">
<p className="mb-4 text-sm text-mineshaft-300">
We&apos;ve developed an improved privilege management system that enhances how access
controls work in your organization.
</p>
<div className="mb-4 rounded-lg border border-mineshaft-600 bg-mineshaft-800 p-4">
<div className="mb-3">
<div className="mb-3">
<p className="mb-3 text-sm text-mineshaft-300">
<strong>Current system: </strong>
Users with higher privilege levels can modify access for anyone below them. This
rigid hierarchy makes it difficult to implement precise access control policies,
forcing you to either over-grant permissions or create complex workarounds when
specialized roles (like team leads) need to manage their team&apos;s access
without receiving broader administrative powers.
</p>
<p className="text-sm text-mineshaft-300">
<strong>New system:</strong> Users need explicit permission to modify specific
access levels, providing targeted control. After upgrading, you&apos;ll need to
grant the new &apos;Grant Privileges&apos; permission. At the organization
level, this is available for the{" "}
<a
href="https://infisical.com/docs/internals/permissions/organization-permissions#subject%3A-groups"
target="_blank"
rel="noreferrer"
className="underline hover:text-bunker-200 hover:decoration-primary-700"
>
Group
</a>{" "}
and{" "}
<a
href="https://infisical.com/docs/internals/permissions/organization-permissions#subject%3A-identity"
target="_blank"
rel="noreferrer"
className="underline hover:text-bunker-200 hover:decoration-primary-700"
>
Identity
</a>{" "}
subjects while at the project level, this is available for the{" "}
<a
href="https://infisical.com/docs/internals/permissions/project-permissions#subject%3A-member"
target="_blank"
rel="noreferrer"
className="underline hover:text-bunker-200 hover:decoration-primary-700"
>
Member,
</a>{" "}
<a
href="https://infisical.com/docs/internals/permissions/project-permissions#subject%3A-groups"
target="_blank"
rel="noreferrer"
className="underline hover:text-bunker-200 hover:decoration-primary-700"
>
Group,
</a>{" "}
and{" "}
<a
href="https://infisical.com/docs/internals/permissions/project-permissions#subject%3A-identity"
target="_blank"
rel="noreferrer"
className="underline hover:text-bunker-200 hover:decoration-primary-700"
>
Identity
</a>{" "}
subjects.
</p>
</div>
</div>
</div>
<div className="mt-4 flex justify-end">
<Button
onClick={() => setStep("upgrade")}
variant="solid"
colorSchema="primary"
size="md"
className="w-[120px] bg-primary hover:bg-primary-600"
>
Continue
</Button>
</div>
</div>
) : (
<div className="mb-4">
<p className="mb-4 text-sm text-mineshaft-300">
Your existing access control workflows will continue to function. However, actions
that involve modifying privileges or permissions will now use the new permission-based
system, requiring users to have explicit permission to modify given resource.
</p>
<p className="mb-4 text-sm text-mineshaft-300">
This upgrade affects operations like updating roles, managing group memberships, and
modifying privileges across your organization and projects.
</p>
<div className="mb-4 rounded-lg border border-mineshaft-600 bg-mineshaft-800 p-4">
<p className="mb-4 text-sm text-mineshaft-300">
Once upgraded, your organization <span className="font-bold">cannot</span> revert to
the legacy privilege system. Please ensure you&apos;ve completed all preparations
before proceeding.
</p>
<div className="mb-4">
<p className="mb-3 text-sm font-medium">Required preparation checklist:</p>
<div className="flex flex-col space-y-4">
<Controller
control={control}
name="isProjectPrivilegesUpdated"
defaultValue={false}
render={({ field: { onBlur, value, onChange }, fieldState: { error } }) => (
<Checkbox
containerClassName="items-start"
className="mt-0.5 items-start"
id="is-project-privileges-updated"
indicatorClassName="flex h-full w-full items-center justify-center"
allowMultilineLabel
isChecked={value}
onCheckedChange={onChange}
onBlur={onBlur}
isError={Boolean(error?.message)}
>
I have reviewed project-level privileges and updated them if necessary
</Checkbox>
)}
/>
<Controller
control={control}
name="isOrgPrivilegesUpdated"
defaultValue={false}
render={({ field: { onBlur, value, onChange }, fieldState: { error } }) => (
<Checkbox
containerClassName="items-start"
className="mt-0.5 items-start"
id="is-org-privileges-updated"
indicatorClassName="flex h-full w-full items-center justify-center"
allowMultilineLabel
isChecked={value}
onCheckedChange={onChange}
onBlur={onBlur}
isError={Boolean(error?.message)}
>
I have reviewed organization-level privileges and updated them if necessary
</Checkbox>
)}
/>
<Controller
control={control}
name="isInfrastructureUpdated"
defaultValue={false}
render={({ field: { onBlur, value, onChange }, fieldState: { error } }) => (
<Checkbox
containerClassName="items-start"
className="mt-0.5 items-start"
id="is-infrastructure-updated"
indicatorClassName="flex h-full w-full items-center justify-center"
allowMultilineLabel
isChecked={value}
onCheckedChange={onChange}
onBlur={onBlur}
isError={Boolean(error?.message)}
>
I have checked Terraform configurations and API integrations for
compatibility with the new system
</Checkbox>
)}
/>
<Controller
control={control}
name="acknowledgesPermanentChange"
defaultValue={false}
render={({ field: { onBlur, value, onChange }, fieldState: { error } }) => (
<Checkbox
containerClassName="items-start"
className="mt-0.5 items-start"
id="acknowledges-permanent-change"
indicatorClassName="flex h-full w-full items-center justify-center"
allowMultilineLabel
isChecked={value}
onCheckedChange={onChange}
onBlur={onBlur}
isError={Boolean(error?.message)}
>
I understand that this upgrade is permanent and cannot be reversed
</Checkbox>
)}
/>
</div>
</div>
</div>
<form onSubmit={handleSubmit(handlePrivilegeSystemUpgrade)}>
<div className="mt-6 flex items-center justify-end gap-4">
<button
type="button"
onClick={() => setStep("info")}
className="w-[120px] text-sm text-mineshaft-300 hover:text-mineshaft-200"
>
Cancel
</button>
<Button
type="submit"
variant="solid"
colorSchema="primary"
size="md"
className="w-[120px] bg-primary hover:bg-primary-600"
isDisabled={!isAllChecksCompleted || !isAdmin}
isLoading={isSubmitting}
>
Upgrade
</Button>
</div>
</form>
</div>
)}
</ModalContent>
</Modal>
);
};
@@ -18,7 +18,7 @@ import {
Tooltip Tooltip
} from "@app/components/v2"; } from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes"; import { ROUTE_PATHS } from "@app/const/routes";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context"; import { OrgPermissionGroupActions, OrgPermissionSubjects } from "@app/context";
import { useDeleteGroup } from "@app/hooks/api"; import { useDeleteGroup } from "@app/hooks/api";
import { useGetGroupById } from "@app/hooks/api/groups/queries"; import { useGetGroupById } from "@app/hooks/api/groups/queries";
import { usePopUp } from "@app/hooks/usePopUp"; import { usePopUp } from "@app/hooks/usePopUp";
@@ -93,7 +93,10 @@ const Page = () => {
</div> </div>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end" className="p-1"> <DropdownMenuContent align="end" className="p-1">
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Groups}> <OrgPermissionCan
I={OrgPermissionGroupActions.Edit}
a={OrgPermissionSubjects.Groups}
>
{(isAllowed) => ( {(isAllowed) => (
<DropdownMenuItem <DropdownMenuItem
className={twMerge( className={twMerge(
@@ -113,7 +116,10 @@ const Page = () => {
</DropdownMenuItem> </DropdownMenuItem>
)} )}
</OrgPermissionCan> </OrgPermissionCan>
<OrgPermissionCan I={OrgPermissionActions.Delete} a={OrgPermissionSubjects.Groups}> <OrgPermissionCan
I={OrgPermissionGroupActions.Delete}
a={OrgPermissionSubjects.Groups}
>
{(isAllowed) => ( {(isAllowed) => (
<DropdownMenuItem <DropdownMenuItem
className={twMerge( className={twMerge(
@@ -179,7 +185,7 @@ export const GroupDetailsByIDPage = () => {
</Helmet> </Helmet>
<OrgPermissionCan <OrgPermissionCan
passThrough={false} passThrough={false}
I={OrgPermissionActions.Read} I={OrgPermissionGroupActions.Read}
a={OrgPermissionSubjects.Groups} a={OrgPermissionSubjects.Groups}
> >
<Page /> <Page />
@@ -20,7 +20,7 @@ import {
THead, THead,
Tr Tr
} from "@app/components/v2"; } from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context"; import { OrgPermissionGroupActions, OrgPermissionSubjects } from "@app/context";
import { useDebounce, useResetPageHelper } from "@app/hooks"; import { useDebounce, useResetPageHelper } from "@app/hooks";
import { useAddUserToGroup, useListGroupUsers } from "@app/hooks/api"; import { useAddUserToGroup, useListGroupUsers } from "@app/hooks/api";
import { EFilterReturnedUsers } from "@app/hooks/api/groups/types"; import { EFilterReturnedUsers } from "@app/hooks/api/groups/types";
@@ -124,7 +124,7 @@ export const AddGroupMembersModal = ({ popUp, handlePopUpToggle }: Props) => {
</Td> </Td>
<Td className="flex justify-end"> <Td className="flex justify-end">
<OrgPermissionCan <OrgPermissionCan
I={OrgPermissionActions.Edit} I={OrgPermissionGroupActions.Edit}
a={OrgPermissionSubjects.Groups} a={OrgPermissionSubjects.Groups}
> >
{(isAllowed) => { {(isAllowed) => {
@@ -4,7 +4,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { OrgPermissionCan } from "@app/components/permissions"; import { OrgPermissionCan } from "@app/components/permissions";
import { IconButton, Spinner, Tooltip } from "@app/components/v2"; import { IconButton, Spinner, Tooltip } from "@app/components/v2";
import { CopyButton } from "@app/components/v2/CopyButton"; import { CopyButton } from "@app/components/v2/CopyButton";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context"; import { OrgPermissionGroupActions, OrgPermissionSubjects } from "@app/context";
import { useGetGroupById } from "@app/hooks/api/"; import { useGetGroupById } from "@app/hooks/api/";
import { UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
@@ -22,7 +22,7 @@ export const GroupDetailsSection = ({ groupId, handlePopUpOpen }: Props) => {
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"> <div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4"> <div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
<h3 className="text-lg font-semibold text-mineshaft-100">Group Details</h3> <h3 className="text-lg font-semibold text-mineshaft-100">Group Details</h3>
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Groups}> <OrgPermissionCan I={OrgPermissionGroupActions.Edit} a={OrgPermissionSubjects.Groups}>
{(isAllowed) => { {(isAllowed) => {
return ( return (
<Tooltip content="Edit Group"> <Tooltip content="Edit Group">
@@ -4,7 +4,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions"; import { OrgPermissionCan } from "@app/components/permissions";
import { DeleteActionModal, IconButton, Tooltip } from "@app/components/v2"; import { DeleteActionModal, IconButton, Tooltip } from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context"; import { OrgPermissionGroupActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { useOidcManageGroupMembershipsEnabled, useRemoveUserFromGroup } from "@app/hooks/api"; import { useOidcManageGroupMembershipsEnabled, useRemoveUserFromGroup } from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp"; import { usePopUp } from "@app/hooks/usePopUp";
@@ -54,7 +54,7 @@ export const GroupMembersSection = ({ groupId, groupSlug }: Props) => {
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"> <div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4"> <div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
<h3 className="text-lg font-semibold text-mineshaft-100">Group Members</h3> <h3 className="text-lg font-semibold text-mineshaft-100">Group Members</h3>
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Groups}> <OrgPermissionCan I={OrgPermissionGroupActions.Edit} a={OrgPermissionSubjects.Groups}>
{(isAllowed) => ( {(isAllowed) => (
<Tooltip <Tooltip
className="text-center" className="text-center"
@@ -24,7 +24,7 @@ import {
Tooltip, Tooltip,
Tr Tr
} from "@app/components/v2"; } from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context"; import { OrgPermissionGroupActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { usePagination, useResetPageHelper } from "@app/hooks"; import { usePagination, useResetPageHelper } from "@app/hooks";
import { useListGroupUsers, useOidcManageGroupMembershipsEnabled } from "@app/hooks/api"; import { useListGroupUsers, useOidcManageGroupMembershipsEnabled } from "@app/hooks/api";
import { OrderByDirection } from "@app/hooks/api/generic/types"; import { OrderByDirection } from "@app/hooks/api/generic/types";
@@ -177,7 +177,7 @@ export const GroupMembersTable = ({ groupId, groupSlug, handlePopUpOpen }: Props
/> />
)} )}
{!groupMemberships?.users.length && ( {!groupMemberships?.users.length && (
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Groups}> <OrgPermissionCan I={OrgPermissionGroupActions.Edit} a={OrgPermissionSubjects.Groups}>
{(isAllowed) => ( {(isAllowed) => (
<Tooltip <Tooltip
className="text-center" className="text-center"
@@ -3,7 +3,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { OrgPermissionCan } from "@app/components/permissions"; import { OrgPermissionCan } from "@app/components/permissions";
import { IconButton, Td, Tooltip, Tr } from "@app/components/v2"; import { IconButton, Td, Tooltip, Tr } from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context"; import { OrgPermissionGroupActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { useOidcManageGroupMembershipsEnabled } from "@app/hooks/api"; import { useOidcManageGroupMembershipsEnabled } from "@app/hooks/api";
import { TGroupUser } from "@app/hooks/api/groups/types"; import { TGroupUser } from "@app/hooks/api/groups/types";
import { UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
@@ -39,7 +39,7 @@ export const GroupMembershipRow = ({
</Tooltip> </Tooltip>
</Td> </Td>
<Td className="justify-end"> <Td className="justify-end">
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Groups}> <OrgPermissionCan I={OrgPermissionGroupActions.Edit} a={OrgPermissionSubjects.Groups}>
{(isAllowed) => { {(isAllowed) => {
return ( return (
<Tooltip <Tooltip
@@ -7,7 +7,7 @@ import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions"; import { OrgPermissionCan } from "@app/components/permissions";
import { DeleteActionModal, PageHeader } from "@app/components/v2"; import { DeleteActionModal, PageHeader } from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes"; import { ROUTE_PATHS } from "@app/const/routes";
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context"; import { OrgPermissionIdentityActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { useDeleteIdentity, useGetIdentityById } from "@app/hooks/api"; import { useDeleteIdentity, useGetIdentityById } from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp"; import { usePopUp } from "@app/hooks/usePopUp";
import { ViewIdentityAuthModal } from "@app/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityAuthModal"; import { ViewIdentityAuthModal } from "@app/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityAuthModal";
@@ -132,7 +132,7 @@ export const IdentityDetailsByIDPage = () => {
</Helmet> </Helmet>
<OrgPermissionCan <OrgPermissionCan
passThrough={false} passThrough={false}
I={OrgPermissionActions.Read} I={OrgPermissionIdentityActions.Read}
a={OrgPermissionSubjects.Identity} a={OrgPermissionSubjects.Identity}
> >
<Page /> <Page />
@@ -3,7 +3,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { OrgPermissionCan } from "@app/components/permissions"; import { OrgPermissionCan } from "@app/components/permissions";
import { Button } from "@app/components/v2"; import { Button } from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context"; import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/context";
import { IdentityAuthMethod, identityAuthToNameMap, useGetIdentityById } from "@app/hooks/api"; import { IdentityAuthMethod, identityAuthToNameMap, useGetIdentityById } from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
@@ -47,7 +47,7 @@ export const IdentityAuthenticationSection = ({ identityId, handlePopUpOpen }: P
{!Object.values(IdentityAuthMethod).every((method) => {!Object.values(IdentityAuthMethod).every((method) =>
data.identity.authMethods.includes(method) data.identity.authMethods.includes(method)
) && ( ) && (
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Identity}> <OrgPermissionCan I={OrgPermissionIdentityActions.Edit} a={OrgPermissionSubjects.Identity}>
{(isAllowed) => ( {(isAllowed) => (
<Button <Button
isDisabled={!isAllowed} isDisabled={!isAllowed}
@@ -4,7 +4,7 @@ import { format } from "date-fns";
import { OrgPermissionCan } from "@app/components/permissions"; import { OrgPermissionCan } from "@app/components/permissions";
import { Button, IconButton, Tooltip } from "@app/components/v2"; import { Button, IconButton, Tooltip } from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context"; import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/context";
import { useTimedReset } from "@app/hooks"; import { useTimedReset } from "@app/hooks";
import { import {
useGetIdentityById, useGetIdentityById,
@@ -118,7 +118,7 @@ export const IdentityClientSecrets = ({ identityId, handlePopUpOpen }: Props) =>
</div> </div>
); );
})} })}
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Identity}> <OrgPermissionCan I={OrgPermissionIdentityActions.Edit} a={OrgPermissionSubjects.Identity}>
{(isAllowed) => { {(isAllowed) => {
return ( return (
<Button <Button
@@ -20,7 +20,7 @@ import {
Tag, Tag,
Tooltip Tooltip
} from "@app/components/v2"; } from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context"; import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/context";
import { useTimedReset } from "@app/hooks"; import { useTimedReset } from "@app/hooks";
import { useGetIdentityById } from "@app/hooks/api"; import { useGetIdentityById } from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
@@ -54,7 +54,10 @@ export const IdentityDetailsSection = ({ identityId, handlePopUpOpen }: Props) =
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent className="min-w-[120px]" align="end"> <DropdownMenuContent className="min-w-[120px]" align="end">
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Identity}> <OrgPermissionCan
I={OrgPermissionIdentityActions.Edit}
a={OrgPermissionSubjects.Identity}
>
{(isAllowed) => ( {(isAllowed) => (
<DropdownMenuItem <DropdownMenuItem
className={twMerge( className={twMerge(
@@ -75,7 +78,10 @@ export const IdentityDetailsSection = ({ identityId, handlePopUpOpen }: Props) =
</DropdownMenuItem> </DropdownMenuItem>
)} )}
</OrgPermissionCan> </OrgPermissionCan>
<OrgPermissionCan I={OrgPermissionActions.Delete} a={OrgPermissionSubjects.Identity}> <OrgPermissionCan
I={OrgPermissionIdentityActions.Delete}
a={OrgPermissionSubjects.Identity}
>
{(isAllowed) => ( {(isAllowed) => (
<DropdownMenuItem <DropdownMenuItem
className={twMerge( className={twMerge(
@@ -20,7 +20,7 @@ import {
Tooltip, Tooltip,
Tr Tr
} from "@app/components/v2"; } from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context"; import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/context";
import { usePopUp } from "@app/hooks"; import { usePopUp } from "@app/hooks";
import { useRevokeIdentityTokenAuthToken } from "@app/hooks/api"; import { useRevokeIdentityTokenAuthToken } from "@app/hooks/api";
import { IdentityAccessToken } from "@app/hooks/api/identities/types"; import { IdentityAccessToken } from "@app/hooks/api/identities/types";
@@ -79,7 +79,7 @@ export const IdentityTokenAuthTokensTable = ({ tokens, identityId }: Props) => {
<div className="col-span-2 mt-3"> <div className="col-span-2 mt-3">
<div className="flex items-end justify-between border-b border-mineshaft-500 pb-2"> <div className="flex items-end justify-between border-b border-mineshaft-500 pb-2">
<span className="text-bunker-300">Access Tokens</span> <span className="text-bunker-300">Access Tokens</span>
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Identity}> <OrgPermissionCan I={OrgPermissionIdentityActions.Edit} a={OrgPermissionSubjects.Identity}>
{(isAllowed) => ( {(isAllowed) => (
<Button <Button
size="xs" size="xs"
@@ -144,7 +144,7 @@ export const IdentityTokenAuthTokensTable = ({ tokens, identityId }: Props) => {
<Td> <Td>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<OrgPermissionCan <OrgPermissionCan
I={OrgPermissionActions.Edit} I={OrgPermissionIdentityActions.Edit}
a={OrgPermissionSubjects.Identity} a={OrgPermissionSubjects.Identity}
> >
{(isAllowed) => ( {(isAllowed) => (
@@ -169,7 +169,7 @@ export const IdentityTokenAuthTokensTable = ({ tokens, identityId }: Props) => {
</OrgPermissionCan> </OrgPermissionCan>
{!isAccessTokenRevoked && ( {!isAccessTokenRevoked && (
<OrgPermissionCan <OrgPermissionCan
I={OrgPermissionActions.Edit} I={OrgPermissionIdentityActions.Edit}
a={OrgPermissionSubjects.Identity} a={OrgPermissionSubjects.Identity}
> >
{(isAllowed) => ( {(isAllowed) => (
@@ -20,7 +20,7 @@ import {
Tooltip, Tooltip,
Tr Tr
} from "@app/components/v2"; } from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context"; import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/context";
import { usePopUp } from "@app/hooks"; import { usePopUp } from "@app/hooks";
import { useRevokeIdentityUniversalAuthClientSecret } from "@app/hooks/api"; import { useRevokeIdentityUniversalAuthClientSecret } from "@app/hooks/api";
import { ClientSecretData } from "@app/hooks/api/identities/types"; import { ClientSecretData } from "@app/hooks/api/identities/types";
@@ -68,7 +68,7 @@ export const IdentityUniversalAuthClientSecretsTable = ({ clientSecrets, identit
<div className="col-span-2"> <div className="col-span-2">
<div className="flex items-end justify-between border-b border-mineshaft-500 pb-2"> <div className="flex items-end justify-between border-b border-mineshaft-500 pb-2">
<span className="text-bunker-300">Client Secrets</span> <span className="text-bunker-300">Client Secrets</span>
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Identity}> <OrgPermissionCan I={OrgPermissionIdentityActions.Edit} a={OrgPermissionSubjects.Identity}>
{(isAllowed) => ( {(isAllowed) => (
<Button <Button
isDisabled={!isAllowed} isDisabled={!isAllowed}
@@ -129,7 +129,7 @@ export const IdentityUniversalAuthClientSecretsTable = ({ clientSecrets, identit
</Td> </Td>
<Td> <Td>
<OrgPermissionCan <OrgPermissionCan
I={OrgPermissionActions.Edit} I={OrgPermissionIdentityActions.Edit}
a={OrgPermissionSubjects.Identity} a={OrgPermissionSubjects.Identity}
> >
{(isAllowed) => ( {(isAllowed) => (
@@ -10,7 +10,7 @@ import {
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger DropdownMenuTrigger
} from "@app/components/v2"; } from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context"; import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/context";
type Props = { type Props = {
children: ReactNode; children: ReactNode;
@@ -36,7 +36,10 @@ export const ViewIdentityContentWrapper = ({ children, onDelete, onEdit }: Props
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent className="min-w-[120px]" align="end"> <DropdownMenuContent className="min-w-[120px]" align="end">
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Identity}> <OrgPermissionCan
I={OrgPermissionIdentityActions.Edit}
a={OrgPermissionSubjects.Identity}
>
{(isAllowed) => ( {(isAllowed) => (
<DropdownMenuItem <DropdownMenuItem
isDisabled={!isAllowed} isDisabled={!isAllowed}
@@ -47,7 +50,10 @@ export const ViewIdentityContentWrapper = ({ children, onDelete, onEdit }: Props
</DropdownMenuItem> </DropdownMenuItem>
)} )}
</OrgPermissionCan> </OrgPermissionCan>
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Identity}> <OrgPermissionCan
I={OrgPermissionIdentityActions.Delete}
a={OrgPermissionSubjects.Identity}
>
{(isAllowed) => ( {(isAllowed) => (
<DropdownMenuItem <DropdownMenuItem
isDisabled={!isAllowed} isDisabled={!isAllowed}
@@ -5,6 +5,8 @@ import { OrgPermissionSubjects } from "@app/context";
import { import {
OrgGatewayPermissionActions, OrgGatewayPermissionActions,
OrgPermissionAppConnectionActions, OrgPermissionAppConnectionActions,
OrgPermissionGroupActions,
OrgPermissionIdentityActions,
OrgPermissionKmipActions, OrgPermissionKmipActions,
OrgPermissionSecretShareAction OrgPermissionSecretShareAction
} from "@app/context/OrgPermissionContext/types"; } from "@app/context/OrgPermissionContext/types";
@@ -36,6 +38,32 @@ const kmipPermissionSchema = z
}) })
.optional(); .optional();
const identityPermissionSchema = z
.object({
[OrgPermissionIdentityActions.Read]: z.boolean().optional(),
[OrgPermissionIdentityActions.Edit]: z.boolean().optional(),
[OrgPermissionIdentityActions.Delete]: z.boolean().optional(),
[OrgPermissionIdentityActions.Create]: z.boolean().optional(),
[OrgPermissionIdentityActions.GrantPrivileges]: z.boolean().optional(),
[OrgPermissionIdentityActions.RevokeAuth]: z.boolean().optional(),
[OrgPermissionIdentityActions.CreateToken]: z.boolean().optional(),
[OrgPermissionIdentityActions.GetToken]: z.boolean().optional(),
[OrgPermissionIdentityActions.DeleteToken]: z.boolean().optional()
})
.optional();
const groupPermissionSchema = z
.object({
[OrgPermissionGroupActions.Read]: z.boolean().optional(),
[OrgPermissionGroupActions.Create]: z.boolean().optional(),
[OrgPermissionGroupActions.Edit]: z.boolean().optional(),
[OrgPermissionGroupActions.Delete]: z.boolean().optional(),
[OrgPermissionGroupActions.GrantPrivileges]: z.boolean().optional(),
[OrgPermissionGroupActions.AddMembers]: z.boolean().optional(),
[OrgPermissionGroupActions.RemoveMembers]: z.boolean().optional()
})
.optional();
const orgGatewayPermissionSchema = z const orgGatewayPermissionSchema = z
.object({ .object({
[OrgGatewayPermissionActions.ListGateways]: z.boolean().optional(), [OrgGatewayPermissionActions.ListGateways]: z.boolean().optional(),
@@ -74,7 +102,7 @@ export const formSchema = z.object({
"audit-logs": generalPermissionSchema, "audit-logs": generalPermissionSchema,
member: generalPermissionSchema, member: generalPermissionSchema,
groups: generalPermissionSchema, groups: groupPermissionSchema,
role: generalPermissionSchema, role: generalPermissionSchema,
settings: generalPermissionSchema, settings: generalPermissionSchema,
"service-account": generalPermissionSchema, "service-account": generalPermissionSchema,
@@ -84,7 +112,7 @@ export const formSchema = z.object({
scim: generalPermissionSchema, scim: generalPermissionSchema,
ldap: generalPermissionSchema, ldap: generalPermissionSchema,
billing: generalPermissionSchema, billing: generalPermissionSchema,
identity: generalPermissionSchema, identity: identityPermissionSchema,
"organization-admin-console": adminConsolePermissionSchmea, "organization-admin-console": adminConsolePermissionSchmea,
[OrgPermissionSubjects.Kms]: generalPermissionSchema, [OrgPermissionSubjects.Kms]: generalPermissionSchema,
[OrgPermissionSubjects.ProjectTemplates]: generalPermissionSchema, [OrgPermissionSubjects.ProjectTemplates]: generalPermissionSchema,
@@ -0,0 +1,207 @@
import { useEffect, useMemo } from "react";
import { Control, Controller, UseFormSetValue, useWatch } from "react-hook-form";
import { faChevronDown, faChevronRight } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications";
import { Checkbox, Select, SelectItem, Td, Tr } from "@app/components/v2";
import { OrgPermissionGroupActions } from "@app/context/OrgPermissionContext/types";
import { useToggle } from "@app/hooks";
import { TFormSchema } from "../OrgRoleModifySection.utils";
const PERMISSION_ACTIONS = [
{ action: OrgPermissionGroupActions.Read, label: "Read Groups" },
{ action: OrgPermissionGroupActions.Create, label: "Create Groups" },
{ action: OrgPermissionGroupActions.Edit, label: "Edit Groups" },
{ action: OrgPermissionGroupActions.Delete, label: "Delete Groups" },
{ action: OrgPermissionGroupActions.GrantPrivileges, label: "Grant Privileges" },
{ action: OrgPermissionGroupActions.AddMembers, label: "Add Members" },
{ action: OrgPermissionGroupActions.RemoveMembers, label: "Remove Members" }
] as const;
type Props = {
isEditable: boolean;
setValue: UseFormSetValue<TFormSchema>;
control: Control<TFormSchema>;
};
enum Permission {
NoAccess = "no-access",
ReadOnly = "read-only",
FullAccess = "full-access",
Custom = "custom"
}
export const OrgPermissionGroupRow = ({ isEditable, control, setValue }: Props) => {
const [isRowExpanded, setIsRowExpanded] = useToggle();
const [isCustom, setIsCustom] = useToggle();
const rule = useWatch({
control,
name: "permissions.groups"
});
const selectedPermissionCategory = useMemo(() => {
const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
const totalActions = PERMISSION_ACTIONS.length;
const score = actions.map((key) => (rule?.[key] ? 1 : 0)).reduce((a, b) => a + b, 0 as number);
if (isCustom) return Permission.Custom;
if (score === 0) return Permission.NoAccess;
if (score === totalActions) return Permission.FullAccess;
if (score === 1 && rule?.read) return Permission.ReadOnly;
return Permission.Custom;
}, [rule, isCustom]);
useEffect(() => {
if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
}, [selectedPermissionCategory]);
useEffect(() => {
const isRowCustom = selectedPermissionCategory === Permission.Custom;
if (isRowCustom) {
setIsRowExpanded.on();
}
}, []);
const handlePermissionChange = (val: Permission) => {
if (val === Permission.Custom) {
setIsRowExpanded.on();
setIsCustom.on();
return;
}
setIsCustom.off();
switch (val) {
case Permission.NoAccess:
setValue(
"permissions.groups",
{
[OrgPermissionGroupActions.Read]: false,
[OrgPermissionGroupActions.Create]: false,
[OrgPermissionGroupActions.Edit]: false,
[OrgPermissionGroupActions.Delete]: false,
[OrgPermissionGroupActions.GrantPrivileges]: false,
[OrgPermissionGroupActions.AddMembers]: false,
[OrgPermissionGroupActions.RemoveMembers]: false
},
{ shouldDirty: true }
);
break;
case Permission.FullAccess:
setValue(
"permissions.groups",
{
[OrgPermissionGroupActions.Read]: true,
[OrgPermissionGroupActions.Create]: true,
[OrgPermissionGroupActions.Edit]: true,
[OrgPermissionGroupActions.Delete]: true,
[OrgPermissionGroupActions.GrantPrivileges]: true,
[OrgPermissionGroupActions.AddMembers]: true,
[OrgPermissionGroupActions.RemoveMembers]: true
},
{ shouldDirty: true }
);
break;
case Permission.ReadOnly:
setValue(
"permissions.groups",
{
[OrgPermissionGroupActions.Read]: true,
[OrgPermissionGroupActions.Edit]: false,
[OrgPermissionGroupActions.Create]: false,
[OrgPermissionGroupActions.Delete]: false,
[OrgPermissionGroupActions.GrantPrivileges]: false,
[OrgPermissionGroupActions.AddMembers]: false,
[OrgPermissionGroupActions.RemoveMembers]: false
},
{ shouldDirty: true }
);
break;
default:
setValue(
"permissions.groups",
{
[OrgPermissionGroupActions.Read]: false,
[OrgPermissionGroupActions.Edit]: false,
[OrgPermissionGroupActions.Create]: false,
[OrgPermissionGroupActions.Delete]: false,
[OrgPermissionGroupActions.GrantPrivileges]: false,
[OrgPermissionGroupActions.AddMembers]: false,
[OrgPermissionGroupActions.RemoveMembers]: false
},
{ shouldDirty: true }
);
break;
}
};
return (
<>
<Tr
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
onClick={() => setIsRowExpanded.toggle()}
>
<Td>
<FontAwesomeIcon icon={isRowExpanded ? faChevronDown : faChevronRight} />
</Td>
<Td>Group Management</Td>
<Td>
<Select
value={selectedPermissionCategory}
className="w-40 bg-mineshaft-600"
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
onValueChange={handlePermissionChange}
isDisabled={!isEditable}
>
<SelectItem value={Permission.NoAccess}>No Access</SelectItem>
<SelectItem value={Permission.ReadOnly}>Read Only</SelectItem>
<SelectItem value={Permission.FullAccess}>Full Access</SelectItem>
<SelectItem value={Permission.Custom}>Custom</SelectItem>
</Select>
</Td>
</Tr>
{isRowExpanded && (
<Tr>
<Td
colSpan={3}
className={`bg-bunker-600 px-0 py-0 ${isRowExpanded && "border-mineshaft-500 p-8"}`}
>
<div className="grid grid-cols-3 gap-4">
{PERMISSION_ACTIONS.map(({ action, label }) => {
return (
<Controller
name={`permissions.groups.${action}`}
key={`permissions.groups.${action}`}
control={control}
render={({ field }) => (
<Checkbox
isChecked={field.value}
onCheckedChange={(e) => {
if (!isEditable) {
createNotification({
type: "error",
text: "Failed to update default role"
});
return;
}
field.onChange(e);
}}
id={`permissions.groups.${action}`}
>
{label}
</Checkbox>
)}
/>
);
})}
</div>
</Td>
</Tr>
)}
</>
);
};
@@ -0,0 +1,217 @@
import { useEffect, useMemo } from "react";
import { Control, Controller, UseFormSetValue, useWatch } from "react-hook-form";
import { faChevronDown, faChevronRight } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications";
import { Checkbox, Select, SelectItem, Td, Tr } from "@app/components/v2";
import { OrgPermissionIdentityActions } from "@app/context/OrgPermissionContext/types";
import { useToggle } from "@app/hooks";
import { TFormSchema } from "../OrgRoleModifySection.utils";
const PERMISSION_ACTIONS = [
{ action: OrgPermissionIdentityActions.Read, label: "Read Identities" },
{ action: OrgPermissionIdentityActions.Create, label: "Create Identities" },
{ action: OrgPermissionIdentityActions.Edit, label: "Edit Identities" },
{ action: OrgPermissionIdentityActions.Delete, label: "Delete Identities" },
{ action: OrgPermissionIdentityActions.GrantPrivileges, label: "Grant Privileges" },
{ action: OrgPermissionIdentityActions.RevokeAuth, label: "Revoke Auth" },
{ action: OrgPermissionIdentityActions.CreateToken, label: "Create Token" },
{ action: OrgPermissionIdentityActions.GetToken, label: "Get Token" },
{ action: OrgPermissionIdentityActions.DeleteToken, label: "Delete Token" }
] as const;
type Props = {
isEditable: boolean;
setValue: UseFormSetValue<TFormSchema>;
control: Control<TFormSchema>;
};
enum Permission {
NoAccess = "no-access",
ReadOnly = "read-only",
FullAccess = "full-access",
Custom = "custom"
}
export const OrgPermissionIdentityRow = ({ isEditable, control, setValue }: Props) => {
const [isRowExpanded, setIsRowExpanded] = useToggle();
const [isCustom, setIsCustom] = useToggle();
const rule = useWatch({
control,
name: "permissions.identity"
});
const selectedPermissionCategory = useMemo(() => {
const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
const totalActions = PERMISSION_ACTIONS.length;
const score = actions.map((key) => (rule?.[key] ? 1 : 0)).reduce((a, b) => a + b, 0 as number);
if (isCustom) return Permission.Custom;
if (score === 0) return Permission.NoAccess;
if (score === totalActions) return Permission.FullAccess;
if (score === 1 && rule?.read) return Permission.ReadOnly;
return Permission.Custom;
}, [rule, isCustom]);
useEffect(() => {
if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
}, [selectedPermissionCategory]);
useEffect(() => {
const isRowCustom = selectedPermissionCategory === Permission.Custom;
if (isRowCustom) {
setIsRowExpanded.on();
}
}, []);
const handlePermissionChange = (val: Permission) => {
if (val === Permission.Custom) {
setIsRowExpanded.on();
setIsCustom.on();
return;
}
setIsCustom.off();
switch (val) {
case Permission.NoAccess:
setValue(
"permissions.identity",
{
[OrgPermissionIdentityActions.Read]: false,
[OrgPermissionIdentityActions.Edit]: false,
[OrgPermissionIdentityActions.Create]: false,
[OrgPermissionIdentityActions.Delete]: false,
[OrgPermissionIdentityActions.GrantPrivileges]: false,
[OrgPermissionIdentityActions.RevokeAuth]: false,
[OrgPermissionIdentityActions.CreateToken]: false,
[OrgPermissionIdentityActions.GetToken]: false,
[OrgPermissionIdentityActions.DeleteToken]: false
},
{ shouldDirty: true }
);
break;
case Permission.FullAccess:
setValue(
"permissions.identity",
{
[OrgPermissionIdentityActions.Read]: true,
[OrgPermissionIdentityActions.Edit]: true,
[OrgPermissionIdentityActions.Create]: true,
[OrgPermissionIdentityActions.Delete]: true,
[OrgPermissionIdentityActions.GrantPrivileges]: true,
[OrgPermissionIdentityActions.RevokeAuth]: true,
[OrgPermissionIdentityActions.CreateToken]: true,
[OrgPermissionIdentityActions.GetToken]: true,
[OrgPermissionIdentityActions.DeleteToken]: true
},
{ shouldDirty: true }
);
break;
case Permission.ReadOnly:
setValue(
"permissions.identity",
{
[OrgPermissionIdentityActions.Read]: true,
[OrgPermissionIdentityActions.Edit]: false,
[OrgPermissionIdentityActions.Create]: false,
[OrgPermissionIdentityActions.Delete]: false,
[OrgPermissionIdentityActions.GrantPrivileges]: false,
[OrgPermissionIdentityActions.RevokeAuth]: false,
[OrgPermissionIdentityActions.CreateToken]: false,
[OrgPermissionIdentityActions.GetToken]: false,
[OrgPermissionIdentityActions.DeleteToken]: false
},
{ shouldDirty: true }
);
break;
default:
setValue(
"permissions.identity",
{
[OrgPermissionIdentityActions.Read]: false,
[OrgPermissionIdentityActions.Edit]: false,
[OrgPermissionIdentityActions.Create]: false,
[OrgPermissionIdentityActions.Delete]: false,
[OrgPermissionIdentityActions.GrantPrivileges]: false,
[OrgPermissionIdentityActions.RevokeAuth]: false,
[OrgPermissionIdentityActions.CreateToken]: false,
[OrgPermissionIdentityActions.GetToken]: false,
[OrgPermissionIdentityActions.DeleteToken]: false
},
{ shouldDirty: true }
);
break;
}
};
return (
<>
<Tr
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
onClick={() => setIsRowExpanded.toggle()}
>
<Td>
<FontAwesomeIcon icon={isRowExpanded ? faChevronDown : faChevronRight} />
</Td>
<Td>Machine Identity Management</Td>
<Td>
<Select
value={selectedPermissionCategory}
className="w-40 bg-mineshaft-600"
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
onValueChange={handlePermissionChange}
isDisabled={!isEditable}
>
<SelectItem value={Permission.NoAccess}>No Access</SelectItem>
<SelectItem value={Permission.ReadOnly}>Read Only</SelectItem>
<SelectItem value={Permission.FullAccess}>Full Access</SelectItem>
<SelectItem value={Permission.Custom}>Custom</SelectItem>
</Select>
</Td>
</Tr>
{isRowExpanded && (
<Tr>
<Td
colSpan={3}
className={`bg-bunker-600 px-0 py-0 ${isRowExpanded && "border-mineshaft-500 p-8"}`}
>
<div className="grid grid-cols-3 gap-4">
{PERMISSION_ACTIONS.map(({ action, label }) => {
return (
<Controller
name={`permissions.identity.${action}`}
key={`permissions.identity.${action}`}
control={control}
render={({ field }) => (
<Checkbox
isChecked={field.value}
onCheckedChange={(e) => {
if (!isEditable) {
createNotification({
type: "error",
text: "Failed to update default role"
});
return;
}
field.onChange(e);
}}
id={`permissions.identity.${action}`}
>
{label}
</Checkbox>
)}
/>
);
})}
</div>
</Td>
</Tr>
)}
</>
);
};
@@ -15,6 +15,8 @@ import {
} from "../OrgRoleModifySection.utils"; } from "../OrgRoleModifySection.utils";
import { OrgPermissionAdminConsoleRow } from "./OrgPermissionAdminConsoleRow"; import { OrgPermissionAdminConsoleRow } from "./OrgPermissionAdminConsoleRow";
import { OrgGatewayPermissionRow } from "./OrgPermissionGatewayRow"; import { OrgGatewayPermissionRow } from "./OrgPermissionGatewayRow";
import { OrgPermissionGroupRow } from "./OrgPermissionGroupRow";
import { OrgPermissionIdentityRow } from "./OrgPermissionIdentityRow";
import { OrgPermissionKmipRow } from "./OrgPermissionKmipRow"; import { OrgPermissionKmipRow } from "./OrgPermissionKmipRow";
import { OrgPermissionSecretShareRow } from "./OrgPermissionSecretShareRow"; import { OrgPermissionSecretShareRow } from "./OrgPermissionSecretShareRow";
import { OrgRoleWorkspaceRow } from "./OrgRoleWorkspaceRow"; import { OrgRoleWorkspaceRow } from "./OrgRoleWorkspaceRow";
@@ -25,14 +27,6 @@ const SIMPLE_PERMISSION_OPTIONS = [
title: "User Management", title: "User Management",
formName: "member" formName: "member"
}, },
{
title: "Group Management",
formName: "groups"
},
{
title: "Machine Identity Management",
formName: "identity"
},
{ {
title: "Usage & Billing", title: "Usage & Billing",
formName: "billing" formName: "billing"
@@ -168,6 +162,16 @@ export const RolePermissionsSection = ({ roleId }: Props) => {
/> />
); );
})} })}
<OrgPermissionIdentityRow
control={control}
setValue={setValue}
isEditable={isCustomRole}
/>
<OrgPermissionGroupRow
control={control}
setValue={setValue}
isEditable={isCustomRole}
/>
<OrgPermissionAppConnectionRow <OrgPermissionAppConnectionRow
control={control} control={control}
setValue={setValue} setValue={setValue}
@@ -140,7 +140,7 @@ const Page = withPermission(
<DropdownMenuContent align="end" className="p-1"> <DropdownMenuContent align="end" className="p-1">
<OrgPermissionCan <OrgPermissionCan
I={OrgPermissionActions.Edit} I={OrgPermissionActions.Edit}
a={OrgPermissionSubjects.Identity} a={OrgPermissionSubjects.Member}
> >
{(isAllowed) => ( {(isAllowed) => (
<DropdownMenuItem <DropdownMenuItem
@@ -82,7 +82,7 @@ export const UserDetailsSection = ({ membershipId, handlePopUpOpen }: Props) =>
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4"> <div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
<h3 className="text-lg font-semibold text-mineshaft-100">User Details</h3> <h3 className="text-lg font-semibold text-mineshaft-100">User Details</h3>
{userId !== membership.user.id && ( {userId !== membership.user.id && (
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Identity}> <OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Member}>
{(isAllowed) => { {(isAllowed) => {
return ( return (
<Tooltip content="Edit Membership"> <Tooltip content="Edit Membership">
@@ -196,7 +196,7 @@ export const UserDetailsSection = ({ membershipId, handlePopUpOpen }: Props) =>
(membership.status === "invited" || membership.status === "verified") && (membership.status === "invited" || membership.status === "verified") &&
membership.user.email && membership.user.email &&
serverDetails?.emailConfigured && ( serverDetails?.emailConfigured && (
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Identity}> <OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Member}>
{(isAllowed) => { {(isAllowed) => {
return ( return (
<Button <Button
@@ -3,8 +3,7 @@ import { useTranslation } from "react-i18next";
import { useNavigate, useSearch } from "@tanstack/react-router"; import { useNavigate, useSearch } from "@tanstack/react-router";
import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; import { useWorkspace } from "@app/context";
import { withProjectPermission } from "@app/hoc";
import { ProjectType } from "@app/hooks/api/workspace/types"; import { ProjectType } from "@app/hooks/api/workspace/types";
import { ProjectAccessControlTabs } from "@app/types/project"; import { ProjectAccessControlTabs } from "@app/types/project";
@@ -16,73 +15,67 @@ import {
ServiceTokenTab ServiceTokenTab
} from "./components"; } from "./components";
const Page = withProjectPermission( const Page = () => {
() => { const navigate = useNavigate();
const navigate = useNavigate(); const { currentWorkspace } = useWorkspace();
const { currentWorkspace } = useWorkspace(); const selectedTab = useSearch({
const selectedTab = useSearch({ strict: false,
strict: false, select: (el) => el.selectedTab
select: (el) => el.selectedTab });
const updateSelectedTab = (tab: string) => {
navigate({
to: `/${currentWorkspace.type}/$projectId/access-management` as const,
search: (prev) => ({ ...prev, selectedTab: tab }),
params: {
projectId: currentWorkspace.id
}
}); });
};
const updateSelectedTab = (tab: string) => { return (
navigate({ <div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
to: `/${currentWorkspace.type}/$projectId/access-management` as const, <div className="mx-auto mb-6 w-full max-w-7xl">
search: (prev) => ({ ...prev, selectedTab: tab }), <PageHeader
params: { title="Access Control"
projectId: currentWorkspace.id description="Manage fine-grained access for users, groups, roles, and identities within your project resources."
} />
}); <Tabs value={selectedTab} onValueChange={updateSelectedTab}>
}; <TabList>
<Tab value={ProjectAccessControlTabs.Member}>Users</Tab>
return ( <Tab value={ProjectAccessControlTabs.Groups}>Groups</Tab>
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white"> <Tab value={ProjectAccessControlTabs.Identities}>
<div className="mx-auto mb-6 w-full max-w-7xl"> <div className="flex items-center">
<PageHeader <p>Machine Identities</p>
title="Access Control" </div>
description="Manage fine-grained access for users, groups, roles, and identities within your project resources." </Tab>
/>
<Tabs value={selectedTab} onValueChange={updateSelectedTab}>
<TabList>
<Tab value={ProjectAccessControlTabs.Member}>Users</Tab>
<Tab value={ProjectAccessControlTabs.Groups}>Groups</Tab>
<Tab value={ProjectAccessControlTabs.Identities}>
<div className="flex items-center">
<p>Machine Identities</p>
</div>
</Tab>
{currentWorkspace?.type === ProjectType.SecretManager && (
<Tab value={ProjectAccessControlTabs.ServiceTokens}>Service Tokens</Tab>
)}
<Tab value={ProjectAccessControlTabs.Roles}>Project Roles</Tab>
</TabList>
<TabPanel value={ProjectAccessControlTabs.Member}>
<MembersTab />
</TabPanel>
<TabPanel value={ProjectAccessControlTabs.Groups}>
<GroupsTab />
</TabPanel>
<TabPanel value={ProjectAccessControlTabs.Identities}>
<IdentityTab />
</TabPanel>
{currentWorkspace?.type === ProjectType.SecretManager && ( {currentWorkspace?.type === ProjectType.SecretManager && (
<TabPanel value={ProjectAccessControlTabs.ServiceTokens}> <Tab value={ProjectAccessControlTabs.ServiceTokens}>Service Tokens</Tab>
<ServiceTokenTab />
</TabPanel>
)} )}
<TabPanel value={ProjectAccessControlTabs.Roles}> <Tab value={ProjectAccessControlTabs.Roles}>Project Roles</Tab>
<ProjectRoleListTab /> </TabList>
<TabPanel value={ProjectAccessControlTabs.Member}>
<MembersTab />
</TabPanel>
<TabPanel value={ProjectAccessControlTabs.Groups}>
<GroupsTab />
</TabPanel>
<TabPanel value={ProjectAccessControlTabs.Identities}>
<IdentityTab />
</TabPanel>
{currentWorkspace?.type === ProjectType.SecretManager && (
<TabPanel value={ProjectAccessControlTabs.ServiceTokens}>
<ServiceTokenTab />
</TabPanel> </TabPanel>
</Tabs> )}
</div> <TabPanel value={ProjectAccessControlTabs.Roles}>
<ProjectRoleListTab />
</TabPanel>
</Tabs>
</div> </div>
); </div>
}, );
{ };
action: ProjectPermissionActions.Read,
subject: ProjectPermissionSub.Member
}
);
export const AccessControlPage = () => { export const AccessControlPage = () => {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -1,17 +1,26 @@
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { withProjectPermission } from "@app/hoc";
import { GroupsSection } from "./components"; import { GroupsSection } from "./components";
export const GroupsTab = () => { export const GroupsTab = withProjectPermission(
return ( () => {
<motion.div return (
key="panel-groups" <motion.div
transition={{ duration: 0.15 }} key="panel-groups"
initial={{ opacity: 0, translateX: 30 }} transition={{ duration: 0.15 }}
animate={{ opacity: 1, translateX: 0 }} initial={{ opacity: 0, translateX: 30 }}
exit={{ opacity: 0, translateX: 30 }} animate={{ opacity: 1, translateX: 0 }}
> exit={{ opacity: 0, translateX: 30 }}
<GroupsSection /> >
</motion.div> <GroupsSection />
); </motion.div>
}; );
},
{
action: ProjectPermissionActions.Read,
subject: ProjectPermissionSub.Groups
}
);
@@ -1,17 +1,26 @@
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { withProjectPermission } from "@app/hoc";
import { MembersSection } from "./components"; import { MembersSection } from "./components";
export const MembersTab = () => { export const MembersTab = withProjectPermission(
return ( () => {
<motion.div return (
key="panel-project-members" <motion.div
transition={{ duration: 0.15 }} key="panel-project-members"
initial={{ opacity: 0, translateX: 30 }} transition={{ duration: 0.15 }}
animate={{ opacity: 1, translateX: 0 }} initial={{ opacity: 0, translateX: 30 }}
exit={{ opacity: 0, translateX: 30 }} animate={{ opacity: 1, translateX: 0 }}
> exit={{ opacity: 0, translateX: 30 }}
<MembersSection /> >
</motion.div> <MembersSection />
); </motion.div>
}; );
},
{
action: ProjectPermissionActions.Read,
subject: ProjectPermissionSub.Member
}
);
@@ -12,7 +12,10 @@ import {
import { import {
PermissionConditionOperators, PermissionConditionOperators,
ProjectPermissionDynamicSecretActions, ProjectPermissionDynamicSecretActions,
ProjectPermissionGroupActions,
ProjectPermissionIdentityActions,
ProjectPermissionKmipActions, ProjectPermissionKmipActions,
ProjectPermissionMemberActions,
ProjectPermissionSecretActions, ProjectPermissionSecretActions,
ProjectPermissionSecretSyncActions, ProjectPermissionSecretSyncActions,
TPermissionCondition, TPermissionCondition,
@@ -71,6 +74,30 @@ const KmipPolicyActionSchema = z.object({
[ProjectPermissionKmipActions.GenerateClientCertificates]: z.boolean().optional() [ProjectPermissionKmipActions.GenerateClientCertificates]: z.boolean().optional()
}); });
const MemberPolicyActionSchema = z.object({
[ProjectPermissionMemberActions.Read]: z.boolean().optional(),
[ProjectPermissionMemberActions.Create]: z.boolean().optional(),
[ProjectPermissionMemberActions.Edit]: z.boolean().optional(),
[ProjectPermissionMemberActions.Delete]: z.boolean().optional(),
[ProjectPermissionMemberActions.GrantPrivileges]: z.boolean().optional()
});
const IdentityPolicyActionSchema = z.object({
[ProjectPermissionIdentityActions.Read]: z.boolean().optional(),
[ProjectPermissionIdentityActions.Create]: z.boolean().optional(),
[ProjectPermissionIdentityActions.Edit]: z.boolean().optional(),
[ProjectPermissionIdentityActions.Delete]: z.boolean().optional(),
[ProjectPermissionIdentityActions.GrantPrivileges]: z.boolean().optional()
});
const GroupPolicyActionSchema = z.object({
[ProjectPermissionGroupActions.Read]: z.boolean().optional(),
[ProjectPermissionGroupActions.Create]: z.boolean().optional(),
[ProjectPermissionGroupActions.Edit]: z.boolean().optional(),
[ProjectPermissionGroupActions.Delete]: z.boolean().optional(),
[ProjectPermissionGroupActions.GrantPrivileges]: z.boolean().optional()
});
const SecretRollbackPolicyActionSchema = z.object({ const SecretRollbackPolicyActionSchema = z.object({
read: z.boolean().optional(), read: z.boolean().optional(),
create: z.boolean().optional() create: z.boolean().optional()
@@ -152,14 +179,14 @@ export const projectRoleFormSchema = z.object({
}) })
.array() .array()
.default([]), .default([]),
[ProjectPermissionSub.Identity]: GeneralPolicyActionSchema.extend({ [ProjectPermissionSub.Identity]: IdentityPolicyActionSchema.extend({
inverted: z.boolean().optional(), inverted: z.boolean().optional(),
conditions: ConditionSchema conditions: ConditionSchema
}) })
.array() .array()
.default([]), .default([]),
[ProjectPermissionSub.Member]: GeneralPolicyActionSchema.array().default([]), [ProjectPermissionSub.Member]: MemberPolicyActionSchema.array().default([]),
[ProjectPermissionSub.Groups]: GeneralPolicyActionSchema.array().default([]), [ProjectPermissionSub.Groups]: GroupPolicyActionSchema.array().default([]),
[ProjectPermissionSub.Role]: GeneralPolicyActionSchema.array().default([]), [ProjectPermissionSub.Role]: GeneralPolicyActionSchema.array().default([]),
[ProjectPermissionSub.Integrations]: GeneralPolicyActionSchema.array().default([]), [ProjectPermissionSub.Integrations]: GeneralPolicyActionSchema.array().default([]),
[ProjectPermissionSub.Webhooks]: GeneralPolicyActionSchema.array().default([]), [ProjectPermissionSub.Webhooks]: GeneralPolicyActionSchema.array().default([]),
@@ -248,9 +275,6 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
ProjectPermissionSub.DynamicSecrets, ProjectPermissionSub.DynamicSecrets,
ProjectPermissionSub.SecretFolders, ProjectPermissionSub.SecretFolders,
ProjectPermissionSub.SecretImports, ProjectPermissionSub.SecretImports,
ProjectPermissionSub.Member,
ProjectPermissionSub.Groups,
ProjectPermissionSub.Identity,
ProjectPermissionSub.Role, ProjectPermissionSub.Role,
ProjectPermissionSub.Integrations, ProjectPermissionSub.Integrations,
ProjectPermissionSub.Webhooks, ProjectPermissionSub.Webhooks,
@@ -432,6 +456,63 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
return; return;
} }
if (subject === ProjectPermissionSub.Member) {
const canRead = action.includes(ProjectPermissionMemberActions.Read);
const canCreate = action.includes(ProjectPermissionMemberActions.Create);
const canEdit = action.includes(ProjectPermissionMemberActions.Edit);
const canDelete = action.includes(ProjectPermissionMemberActions.Delete);
const canGrantPrivileges = action.includes(ProjectPermissionMemberActions.GrantPrivileges);
if (!formVal[subject]) formVal[subject] = [{}];
// from above statement we are sure it won't be undefined
if (canRead) formVal[subject]![0][ProjectPermissionMemberActions.Read] = true;
if (canCreate) formVal[subject]![0][ProjectPermissionMemberActions.Create] = true;
if (canEdit) formVal[subject]![0][ProjectPermissionMemberActions.Edit] = true;
if (canDelete) formVal[subject]![0][ProjectPermissionMemberActions.Delete] = true;
if (canGrantPrivileges)
formVal[subject]![0][ProjectPermissionMemberActions.GrantPrivileges] = true;
return;
}
if (subject === ProjectPermissionSub.Identity) {
const canRead = action.includes(ProjectPermissionIdentityActions.Read);
const canCreate = action.includes(ProjectPermissionIdentityActions.Create);
const canEdit = action.includes(ProjectPermissionIdentityActions.Edit);
const canDelete = action.includes(ProjectPermissionIdentityActions.Delete);
const canGrantPrivileges = action.includes(ProjectPermissionIdentityActions.GrantPrivileges);
if (!formVal[subject]) formVal[subject] = [{ conditions: [] }];
// from above statement we are sure it won't be undefined
if (canRead) formVal[subject]![0][ProjectPermissionIdentityActions.Read] = true;
if (canCreate) formVal[subject]![0][ProjectPermissionIdentityActions.Create] = true;
if (canEdit) formVal[subject]![0][ProjectPermissionIdentityActions.Edit] = true;
if (canDelete) formVal[subject]![0][ProjectPermissionIdentityActions.Delete] = true;
if (canGrantPrivileges)
formVal[subject]![0][ProjectPermissionIdentityActions.GrantPrivileges] = true;
return;
}
if (subject === ProjectPermissionSub.Groups) {
const canRead = action.includes(ProjectPermissionGroupActions.Read);
const canCreate = action.includes(ProjectPermissionGroupActions.Create);
const canEdit = action.includes(ProjectPermissionGroupActions.Edit);
const canDelete = action.includes(ProjectPermissionGroupActions.Delete);
const canGrantPrivileges = action.includes(ProjectPermissionGroupActions.GrantPrivileges);
if (!formVal[subject]) formVal[subject] = [{}];
// from above statement we are sure it won't be undefined
if (canRead) formVal[subject]![0][ProjectPermissionGroupActions.Read] = true;
if (canCreate) formVal[subject]![0][ProjectPermissionGroupActions.Create] = true;
if (canEdit) formVal[subject]![0][ProjectPermissionGroupActions.Edit] = true;
if (canDelete) formVal[subject]![0][ProjectPermissionGroupActions.Delete] = true;
if (canGrantPrivileges)
formVal[subject]![0][ProjectPermissionGroupActions.GrantPrivileges] = true;
return;
}
if (subject === ProjectPermissionSub.SecretSyncs) { if (subject === ProjectPermissionSub.SecretSyncs) {
const canRead = action.includes(ProjectPermissionSecretSyncActions.Read); const canRead = action.includes(ProjectPermissionSecretSyncActions.Read);
const canEdit = action.includes(ProjectPermissionSecretSyncActions.Edit); const canEdit = action.includes(ProjectPermissionSecretSyncActions.Edit);
@@ -637,28 +718,31 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
[ProjectPermissionSub.Member]: { [ProjectPermissionSub.Member]: {
title: "User Management", title: "User Management",
actions: [ actions: [
{ label: "View all members", value: "read" }, { label: "Read", value: ProjectPermissionMemberActions.Read },
{ label: "Invite members", value: "create" }, { label: "Add", value: ProjectPermissionMemberActions.Create },
{ label: "Edit members", value: "edit" }, { label: "Modify", value: ProjectPermissionMemberActions.Edit },
{ label: "Remove members", value: "delete" } { label: "Remove", value: ProjectPermissionMemberActions.Delete },
{ label: "Grant Privileges", value: ProjectPermissionMemberActions.GrantPrivileges }
] ]
}, },
[ProjectPermissionSub.Identity]: { [ProjectPermissionSub.Identity]: {
title: "Machine Identity Management", title: "Machine Identity Management",
actions: [ actions: [
{ label: "Read", value: "read" }, { label: "Read", value: ProjectPermissionIdentityActions.Read },
{ label: "Add", value: "create" }, { label: "Add", value: ProjectPermissionIdentityActions.Create },
{ label: "Modify", value: "edit" }, { label: "Modify", value: ProjectPermissionIdentityActions.Edit },
{ label: "Remove", value: "delete" } { label: "Remove", value: ProjectPermissionIdentityActions.Delete },
{ label: "Grant Privileges", value: ProjectPermissionIdentityActions.GrantPrivileges }
] ]
}, },
[ProjectPermissionSub.Groups]: { [ProjectPermissionSub.Groups]: {
title: "Group Management", title: "Group Management",
actions: [ actions: [
{ label: "Read", value: "read" }, { label: "Read", value: ProjectPermissionGroupActions.Read },
{ label: "Create", value: "create" }, { label: "Create", value: ProjectPermissionGroupActions.Create },
{ label: "Modify", value: "edit" }, { label: "Modify", value: ProjectPermissionGroupActions.Edit },
{ label: "Remove", value: "delete" } { label: "Remove", value: ProjectPermissionGroupActions.Delete },
{ label: "Grant Privileges", value: ProjectPermissionGroupActions.GrantPrivileges }
] ]
}, },
[ProjectPermissionSub.Webhooks]: { [ProjectPermissionSub.Webhooks]: {