mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 07:26:45 +00:00
feat: completed new permission boundary check
This commit is contained in:
@@ -40,6 +40,7 @@
|
|||||||
"type:check": "tsc --noEmit",
|
"type:check": "tsc --noEmit",
|
||||||
"lint:fix": "eslint --fix --ext js,ts ./src",
|
"lint:fix": "eslint --fix --ext js,ts ./src",
|
||||||
"lint": "eslint 'src/**/*.ts'",
|
"lint": "eslint 'src/**/*.ts'",
|
||||||
|
"test:unit": "vitest run -c vitest.unit.config.ts",
|
||||||
"test:e2e": "vitest run -c vitest.e2e.config.ts --bail=1",
|
"test:e2e": "vitest run -c vitest.e2e.config.ts --bail=1",
|
||||||
"test:e2e-watch": "vitest -c vitest.e2e.config.ts --bail=1",
|
"test:e2e-watch": "vitest -c vitest.e2e.config.ts --bail=1",
|
||||||
"test:e2e-coverage": "vitest run --coverage -c vitest.e2e.config.ts",
|
"test:e2e-coverage": "vitest run --coverage -c vitest.e2e.config.ts",
|
||||||
|
|||||||
@@ -3,7 +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 { isAtLeastAsPrivileged } from "@app/lib/casl";
|
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
|
||||||
import { BadRequestError, ForbiddenRequestError, NotFoundError, 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";
|
||||||
@@ -87,9 +87,14 @@ export const groupServiceFactory = ({
|
|||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
const isCustomRole = Boolean(customRole);
|
const isCustomRole = Boolean(customRole);
|
||||||
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, rolePermission);
|
|
||||||
if (!hasRequiredPriviledges)
|
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
|
||||||
throw new ForbiddenRequestError({ message: "Failed to create a more privileged group" });
|
if (!permissionBoundary.isValid)
|
||||||
|
throw new ForbiddenRequestError({
|
||||||
|
name: "PermissionBoundaryError",
|
||||||
|
message: "Failed to create a more privileged group",
|
||||||
|
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);
|
||||||
@@ -156,9 +161,13 @@ export const groupServiceFactory = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const isCustomRole = Boolean(customOrgRole);
|
const isCustomRole = Boolean(customOrgRole);
|
||||||
const hasRequiredNewRolePermission = isAtLeastAsPrivileged(permission, rolePermission);
|
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
|
||||||
if (!hasRequiredNewRolePermission)
|
if (!permissionBoundary.isValid)
|
||||||
throw new ForbiddenRequestError({ message: "Failed to create a more privileged group" });
|
throw new ForbiddenRequestError({
|
||||||
|
name: "PermissionBoundaryError",
|
||||||
|
message: "Failed to create a more privileged group",
|
||||||
|
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||||
|
});
|
||||||
if (isCustomRole) customRole = customOrgRole;
|
if (isCustomRole) customRole = customOrgRole;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -329,9 +338,13 @@ 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 hasRequiredPrivileges = isAtLeastAsPrivileged(permission, groupRolePermission);
|
const permissionBoundary = validatePermissionBoundary(permission, groupRolePermission);
|
||||||
if (!hasRequiredPrivileges)
|
if (!permissionBoundary.isValid)
|
||||||
throw new ForbiddenRequestError({ message: "Failed to add user to more privileged group" });
|
throw new ForbiddenRequestError({
|
||||||
|
name: "PermissionBoundaryError",
|
||||||
|
message: "Failed to add user to more privileged group",
|
||||||
|
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||||
|
});
|
||||||
|
|
||||||
const user = await userDAL.findOne({ username });
|
const user = await userDAL.findOne({ username });
|
||||||
if (!user) throw new NotFoundError({ message: `Failed to find user with username ${username}` });
|
if (!user) throw new NotFoundError({ message: `Failed to find user with username ${username}` });
|
||||||
@@ -396,9 +409,13 @@ 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 hasRequiredPrivileges = isAtLeastAsPrivileged(permission, groupRolePermission);
|
const permissionBoundary = validatePermissionBoundary(permission, groupRolePermission);
|
||||||
if (!hasRequiredPrivileges)
|
if (!permissionBoundary.isValid)
|
||||||
throw new ForbiddenRequestError({ message: "Failed to delete user from more privileged group" });
|
throw new ForbiddenRequestError({
|
||||||
|
name: "PermissionBoundaryError",
|
||||||
|
message: "Failed to delete user from more privileged group",
|
||||||
|
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||||
|
});
|
||||||
|
|
||||||
const user = await userDAL.findOne({ username });
|
const user = await userDAL.findOne({ username });
|
||||||
if (!user) throw new NotFoundError({ message: `Failed to find user with username ${username}` });
|
if (!user) throw new NotFoundError({ message: `Failed to find user with username ${username}` });
|
||||||
|
|||||||
+22
-10
@@ -3,7 +3,7 @@ import { packRules } from "@casl/ability/extra";
|
|||||||
import ms from "ms";
|
import ms from "ms";
|
||||||
|
|
||||||
import { ActionProjectType, TableName } from "@app/db/schemas";
|
import { ActionProjectType, TableName } from "@app/db/schemas";
|
||||||
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
|
||||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
import { unpackPermissions } from "@app/server/routes/santizedSchemas/permission";
|
import { unpackPermissions } from "@app/server/routes/santizedSchemas/permission";
|
||||||
import { ActorType } from "@app/services/auth/auth-type";
|
import { ActorType } from "@app/services/auth/auth-type";
|
||||||
@@ -79,9 +79,13 @@ 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 hasRequiredPriviledges = isAtLeastAsPrivileged(permission, targetIdentityPermission);
|
const permissionBoundary = validatePermissionBoundary(permission, targetIdentityPermission);
|
||||||
if (!hasRequiredPriviledges)
|
if (!permissionBoundary.isValid)
|
||||||
throw new ForbiddenRequestError({ message: "Failed to update more privileged identity" });
|
throw new ForbiddenRequestError({
|
||||||
|
name: "PermissionBoundaryError",
|
||||||
|
message: "Failed to update more privileged identity",
|
||||||
|
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||||
|
});
|
||||||
|
|
||||||
const existingSlug = await identityProjectAdditionalPrivilegeDAL.findOne({
|
const existingSlug = await identityProjectAdditionalPrivilegeDAL.findOne({
|
||||||
slug,
|
slug,
|
||||||
@@ -161,9 +165,13 @@ 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 hasRequiredPriviledges = isAtLeastAsPrivileged(permission, targetIdentityPermission);
|
const permissionBoundary = validatePermissionBoundary(permission, targetIdentityPermission);
|
||||||
if (!hasRequiredPriviledges)
|
if (!permissionBoundary.isValid)
|
||||||
throw new ForbiddenRequestError({ message: "Failed to update more privileged identity" });
|
throw new ForbiddenRequestError({
|
||||||
|
name: "PermissionBoundaryError",
|
||||||
|
message: "Failed to update more privileged identity",
|
||||||
|
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||||
|
});
|
||||||
|
|
||||||
if (data?.slug) {
|
if (data?.slug) {
|
||||||
const existingSlug = await identityProjectAdditionalPrivilegeDAL.findOne({
|
const existingSlug = await identityProjectAdditionalPrivilegeDAL.findOne({
|
||||||
@@ -239,9 +247,13 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
|
|||||||
actorOrgId,
|
actorOrgId,
|
||||||
actionProjectType: ActionProjectType.Any
|
actionProjectType: ActionProjectType.Any
|
||||||
});
|
});
|
||||||
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, identityRolePermission);
|
const permissionBoundary = validatePermissionBoundary(permission, identityRolePermission);
|
||||||
if (!hasRequiredPriviledges)
|
if (!permissionBoundary.isValid)
|
||||||
throw new ForbiddenRequestError({ message: "Failed to update more privileged identity" });
|
throw new ForbiddenRequestError({
|
||||||
|
name: "PermissionBoundaryError",
|
||||||
|
message: "Failed to update more privileged identity",
|
||||||
|
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||||
|
});
|
||||||
|
|
||||||
const deletedPrivilege = await identityProjectAdditionalPrivilegeDAL.deleteById(identityPrivilege.id);
|
const deletedPrivilege = await identityProjectAdditionalPrivilegeDAL.deleteById(identityPrivilege.id);
|
||||||
return {
|
return {
|
||||||
|
|||||||
+22
-10
@@ -3,7 +3,7 @@ import { PackRule, packRules, unpackRules } from "@casl/ability/extra";
|
|||||||
import ms from "ms";
|
import ms from "ms";
|
||||||
|
|
||||||
import { ActionProjectType } from "@app/db/schemas";
|
import { ActionProjectType } from "@app/db/schemas";
|
||||||
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
|
||||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
import { UnpackedPermissionSchema } from "@app/server/routes/santizedSchemas/permission";
|
import { UnpackedPermissionSchema } from "@app/server/routes/santizedSchemas/permission";
|
||||||
import { ActorType } from "@app/services/auth/auth-type";
|
import { ActorType } from "@app/services/auth/auth-type";
|
||||||
@@ -88,9 +88,13 @@ 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 hasRequiredPriviledges = isAtLeastAsPrivileged(permission, targetIdentityPermission);
|
const permissionBoundary = validatePermissionBoundary(permission, targetIdentityPermission);
|
||||||
if (!hasRequiredPriviledges)
|
if (!permissionBoundary.isValid)
|
||||||
throw new ForbiddenRequestError({ message: "Failed to update more privileged identity" });
|
throw new ForbiddenRequestError({
|
||||||
|
name: "PermissionBoundaryError",
|
||||||
|
message: "Failed to update more privileged identity",
|
||||||
|
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||||
|
});
|
||||||
|
|
||||||
const existingSlug = await identityProjectAdditionalPrivilegeDAL.findOne({
|
const existingSlug = await identityProjectAdditionalPrivilegeDAL.findOne({
|
||||||
slug,
|
slug,
|
||||||
@@ -172,9 +176,13 @@ 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 hasRequiredPriviledges = isAtLeastAsPrivileged(permission, targetIdentityPermission);
|
const permissionBoundary = validatePermissionBoundary(permission, targetIdentityPermission);
|
||||||
if (!hasRequiredPriviledges)
|
if (!permissionBoundary.isValid)
|
||||||
throw new ForbiddenRequestError({ message: "Failed to update more privileged identity" });
|
throw new ForbiddenRequestError({
|
||||||
|
name: "PermissionBoundaryError",
|
||||||
|
message: "Failed to update more privileged identity",
|
||||||
|
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||||
|
});
|
||||||
|
|
||||||
const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findOne({
|
const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findOne({
|
||||||
slug,
|
slug,
|
||||||
@@ -268,9 +276,13 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
|
|||||||
actorOrgId,
|
actorOrgId,
|
||||||
actionProjectType: ActionProjectType.Any
|
actionProjectType: ActionProjectType.Any
|
||||||
});
|
});
|
||||||
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, identityRolePermission);
|
const permissionBoundary = validatePermissionBoundary(permission, identityRolePermission);
|
||||||
if (!hasRequiredPriviledges)
|
if (!permissionBoundary.isValid)
|
||||||
throw new ForbiddenRequestError({ message: "Failed to edit more privileged identity" });
|
throw new ForbiddenRequestError({
|
||||||
|
name: "PermissionBoundaryError",
|
||||||
|
message: "Failed to edit more privileged identity",
|
||||||
|
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||||
|
});
|
||||||
|
|
||||||
const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findOne({
|
const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findOne({
|
||||||
slug,
|
slug,
|
||||||
|
|||||||
+15
-7
@@ -3,7 +3,7 @@ import { PackRule, packRules, unpackRules } from "@casl/ability/extra";
|
|||||||
import ms from "ms";
|
import ms from "ms";
|
||||||
|
|
||||||
import { ActionProjectType, TableName } from "@app/db/schemas";
|
import { ActionProjectType, TableName } from "@app/db/schemas";
|
||||||
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
|
||||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
import { UnpackedPermissionSchema } from "@app/server/routes/santizedSchemas/permission";
|
import { UnpackedPermissionSchema } from "@app/server/routes/santizedSchemas/permission";
|
||||||
import { ActorType } from "@app/services/auth/auth-type";
|
import { ActorType } from "@app/services/auth/auth-type";
|
||||||
@@ -76,9 +76,13 @@ 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 hasRequiredPriviledges = isAtLeastAsPrivileged(permission, targetUserPermission);
|
const permissionBoundary = validatePermissionBoundary(permission, targetUserPermission);
|
||||||
if (!hasRequiredPriviledges)
|
if (!permissionBoundary.isValid)
|
||||||
throw new ForbiddenRequestError({ message: "Failed to update more privileged identity" });
|
throw new ForbiddenRequestError({
|
||||||
|
name: "PermissionBoundaryError",
|
||||||
|
message: "Failed to update more privileged user",
|
||||||
|
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||||
|
});
|
||||||
|
|
||||||
const existingSlug = await projectUserAdditionalPrivilegeDAL.findOne({
|
const existingSlug = await projectUserAdditionalPrivilegeDAL.findOne({
|
||||||
slug,
|
slug,
|
||||||
@@ -163,9 +167,13 @@ 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 hasRequiredPriviledges = isAtLeastAsPrivileged(permission, targetUserPermission);
|
const permissionBoundary = validatePermissionBoundary(permission, targetUserPermission);
|
||||||
if (!hasRequiredPriviledges)
|
if (!permissionBoundary.isValid)
|
||||||
throw new ForbiddenRequestError({ message: "Failed to update more privileged identity" });
|
throw new ForbiddenRequestError({
|
||||||
|
name: "PermissionBoundaryError",
|
||||||
|
message: "Failed to update more privileged identity",
|
||||||
|
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||||
|
});
|
||||||
|
|
||||||
if (dto?.slug) {
|
if (dto?.slug) {
|
||||||
const existingSlug = await projectUserAdditionalPrivilegeDAL.findOne({
|
const existingSlug = await projectUserAdditionalPrivilegeDAL.findOne({
|
||||||
|
|||||||
@@ -0,0 +1,665 @@
|
|||||||
|
import { createMongoAbility } from "@casl/ability";
|
||||||
|
|
||||||
|
import { PermissionConditionOperators } from ".";
|
||||||
|
import { validatePermissionBoundary } from "./boundary";
|
||||||
|
|
||||||
|
describe("Validate Permission Boundary Function", () => {
|
||||||
|
test.each([
|
||||||
|
{
|
||||||
|
title: "child with equal privilege",
|
||||||
|
parentPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create", "edit", "delete", "read"],
|
||||||
|
subject: "secrets"
|
||||||
|
}
|
||||||
|
]),
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create", "edit", "delete", "read"],
|
||||||
|
subject: "secrets"
|
||||||
|
}
|
||||||
|
]),
|
||||||
|
expectValid: true,
|
||||||
|
missingPermissions: []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "child with less privilege",
|
||||||
|
parentPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create", "edit", "delete", "read"],
|
||||||
|
subject: "secrets"
|
||||||
|
}
|
||||||
|
]),
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create", "edit"],
|
||||||
|
subject: "secrets"
|
||||||
|
}
|
||||||
|
]),
|
||||||
|
expectValid: true,
|
||||||
|
missingPermissions: []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "child with more privilege",
|
||||||
|
parentPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create"],
|
||||||
|
subject: "secrets"
|
||||||
|
}
|
||||||
|
]),
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create", "edit"],
|
||||||
|
subject: "secrets"
|
||||||
|
}
|
||||||
|
]),
|
||||||
|
expectValid: false,
|
||||||
|
missingPermissions: [{ action: "edit", subject: "secrets" }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "parent with multiple and child with multiple",
|
||||||
|
parentPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create"],
|
||||||
|
subject: "secrets"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: ["create", "edit"],
|
||||||
|
subject: "members"
|
||||||
|
}
|
||||||
|
]),
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create"],
|
||||||
|
subject: "members"
|
||||||
|
}
|
||||||
|
]),
|
||||||
|
expectValid: true,
|
||||||
|
missingPermissions: []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Child with no access",
|
||||||
|
parentPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create"],
|
||||||
|
subject: "secrets"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: ["create", "edit"],
|
||||||
|
subject: "members"
|
||||||
|
}
|
||||||
|
]),
|
||||||
|
childPermission: createMongoAbility([]),
|
||||||
|
expectValid: true,
|
||||||
|
missingPermissions: []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Parent and child disjoint set",
|
||||||
|
parentPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create", "edit", "delete", "read"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
environment: { [PermissionConditionOperators.$EQ]: "dev" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]),
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create", "edit", "delete", "read"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
secretPath: { [PermissionConditionOperators.$EQ]: "dev" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]),
|
||||||
|
expectValid: false,
|
||||||
|
missingPermissions: ["create", "edit", "delete", "read"].map((el) => ({
|
||||||
|
action: el,
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
secretPath: { [PermissionConditionOperators.$EQ]: "dev" }
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Parent with inverted rules",
|
||||||
|
parentPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create", "edit", "delete", "read"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
environment: { [PermissionConditionOperators.$EQ]: "dev" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: "read",
|
||||||
|
subject: "secrets",
|
||||||
|
inverted: true,
|
||||||
|
conditions: {
|
||||||
|
environment: { [PermissionConditionOperators.$EQ]: "dev" },
|
||||||
|
secretPath: { [PermissionConditionOperators.$GLOB]: "/hello/**" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]),
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: "read",
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
environment: { [PermissionConditionOperators.$EQ]: "dev" },
|
||||||
|
secretPath: { [PermissionConditionOperators.$EQ]: "/" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]),
|
||||||
|
expectValid: true,
|
||||||
|
missingPermissions: []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Parent with inverted rules - child accessing invalid one",
|
||||||
|
parentPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create", "edit", "delete", "read"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
environment: { [PermissionConditionOperators.$EQ]: "dev" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: "read",
|
||||||
|
subject: "secrets",
|
||||||
|
inverted: true,
|
||||||
|
conditions: {
|
||||||
|
environment: { [PermissionConditionOperators.$EQ]: "dev" },
|
||||||
|
secretPath: { [PermissionConditionOperators.$GLOB]: "/hello/**" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]),
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: "read",
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
environment: { [PermissionConditionOperators.$EQ]: "dev" },
|
||||||
|
secretPath: { [PermissionConditionOperators.$EQ]: "/hello/world" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]),
|
||||||
|
expectValid: false,
|
||||||
|
missingPermissions: [
|
||||||
|
{
|
||||||
|
action: "read",
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
environment: { [PermissionConditionOperators.$EQ]: "dev" },
|
||||||
|
secretPath: { [PermissionConditionOperators.$EQ]: "/hello/world" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
])("Check permission: $title", ({ parentPermission, childPermission, expectValid, missingPermissions }) => {
|
||||||
|
const permissionBoundary = validatePermissionBoundary(parentPermission, childPermission);
|
||||||
|
if (expectValid) {
|
||||||
|
expect(permissionBoundary.isValid).toBeTruthy();
|
||||||
|
} else {
|
||||||
|
expect(permissionBoundary.isValid).toBeFalsy();
|
||||||
|
expect(permissionBoundary.missingPermissions).toEqual(expect.arrayContaining(missingPermissions));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Validate Permission Boundary: Checking Parent $eq operator", () => {
|
||||||
|
const parentPermission = createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create", "read"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
environment: { [PermissionConditionOperators.$EQ]: "dev" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
{
|
||||||
|
operator: PermissionConditionOperators.$EQ,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
environment: { [PermissionConditionOperators.$EQ]: "dev" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
operator: PermissionConditionOperators.$IN,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
environment: { [PermissionConditionOperators.$IN]: ["dev"] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
operator: PermissionConditionOperators.$GLOB,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
environment: { [PermissionConditionOperators.$GLOB]: "dev" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
}
|
||||||
|
])("Child $operator truthy cases", ({ childPermission }) => {
|
||||||
|
const permissionBoundary = validatePermissionBoundary(parentPermission, childPermission);
|
||||||
|
expect(permissionBoundary.isValid).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
{
|
||||||
|
operator: PermissionConditionOperators.$EQ,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
environment: { [PermissionConditionOperators.$EQ]: "prod" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
operator: PermissionConditionOperators.$IN,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
environment: { [PermissionConditionOperators.$IN]: ["dev", "prod"] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
operator: PermissionConditionOperators.$GLOB,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
environment: { [PermissionConditionOperators.$GLOB]: "dev**" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
operator: PermissionConditionOperators.$NEQ,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
environment: { [PermissionConditionOperators.$GLOB]: "staging" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
}
|
||||||
|
])("Child $operator falsy cases", ({ childPermission }) => {
|
||||||
|
const permissionBoundary = validatePermissionBoundary(parentPermission, childPermission);
|
||||||
|
expect(permissionBoundary.isValid).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Validate Permission Boundary: Checking Parent $neq operator", () => {
|
||||||
|
const parentPermission = createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create", "read"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
secretPath: { [PermissionConditionOperators.$NEQ]: "/hello" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
{
|
||||||
|
operator: PermissionConditionOperators.$EQ,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
secretPath: { [PermissionConditionOperators.$EQ]: "/" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
operator: PermissionConditionOperators.$NEQ,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
secretPath: { [PermissionConditionOperators.$NEQ]: "/hello" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
operator: PermissionConditionOperators.$IN,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
secretPath: { [PermissionConditionOperators.$IN]: ["/", "/staging"] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
operator: PermissionConditionOperators.$GLOB,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
secretPath: { [PermissionConditionOperators.$GLOB]: "/dev**" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
}
|
||||||
|
])("Child $operator truthy cases", ({ childPermission }) => {
|
||||||
|
const permissionBoundary = validatePermissionBoundary(parentPermission, childPermission);
|
||||||
|
expect(permissionBoundary.isValid).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
{
|
||||||
|
operator: PermissionConditionOperators.$EQ,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
secretPath: { [PermissionConditionOperators.$EQ]: "/hello" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
operator: PermissionConditionOperators.$NEQ,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
secretPath: { [PermissionConditionOperators.$NEQ]: "/" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
operator: PermissionConditionOperators.$IN,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
secretPath: { [PermissionConditionOperators.$IN]: ["/", "/hello"] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
operator: PermissionConditionOperators.$GLOB,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
secretPath: { [PermissionConditionOperators.$GLOB]: "/hello**" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
}
|
||||||
|
])("Child $operator falsy cases", ({ childPermission }) => {
|
||||||
|
const permissionBoundary = validatePermissionBoundary(parentPermission, childPermission);
|
||||||
|
expect(permissionBoundary.isValid).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Validate Permission Boundary: Checking Parent $IN operator", () => {
|
||||||
|
const parentPermission = createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["edit"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
environment: { [PermissionConditionOperators.$IN]: ["dev", "staging"] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
{
|
||||||
|
operator: PermissionConditionOperators.$EQ,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["edit"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
environment: { [PermissionConditionOperators.$EQ]: "dev" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
operator: PermissionConditionOperators.$IN,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["edit"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
environment: { [PermissionConditionOperators.$IN]: ["dev"] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
operator: `${PermissionConditionOperators.$IN} - 2`,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["edit"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
environment: { [PermissionConditionOperators.$IN]: ["dev", "staging"] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
operator: PermissionConditionOperators.$GLOB,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["edit"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
environment: { [PermissionConditionOperators.$GLOB]: "dev" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
}
|
||||||
|
])("Child $operator truthy cases", ({ childPermission }) => {
|
||||||
|
const permissionBoundary = validatePermissionBoundary(parentPermission, childPermission);
|
||||||
|
expect(permissionBoundary.isValid).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
{
|
||||||
|
operator: PermissionConditionOperators.$EQ,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["edit"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
environment: { [PermissionConditionOperators.$EQ]: "prod" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
operator: PermissionConditionOperators.$NEQ,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["edit"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
environment: { [PermissionConditionOperators.$NEQ]: "dev" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
operator: PermissionConditionOperators.$IN,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["edit"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
environment: { [PermissionConditionOperators.$IN]: ["dev", "prod"] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
operator: PermissionConditionOperators.$GLOB,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["edit"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
environment: { [PermissionConditionOperators.$GLOB]: "dev**" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
}
|
||||||
|
])("Child $operator falsy cases", ({ childPermission }) => {
|
||||||
|
const permissionBoundary = validatePermissionBoundary(parentPermission, childPermission);
|
||||||
|
expect(permissionBoundary.isValid).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Validate Permission Boundary: Checking Parent $GLOB operator", () => {
|
||||||
|
const parentPermission = createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create", "read"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
secretPath: { [PermissionConditionOperators.$GLOB]: "/hello/**" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
{
|
||||||
|
operator: PermissionConditionOperators.$EQ,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
secretPath: { [PermissionConditionOperators.$EQ]: "/hello/world" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
operator: PermissionConditionOperators.$IN,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
secretPath: { [PermissionConditionOperators.$IN]: ["/hello/world", "/hello/world2"] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
operator: PermissionConditionOperators.$GLOB,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
secretPath: { [PermissionConditionOperators.$GLOB]: "/hello/**/world" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
}
|
||||||
|
])("Child $operator truthy cases", ({ childPermission }) => {
|
||||||
|
const permissionBoundary = validatePermissionBoundary(parentPermission, childPermission);
|
||||||
|
expect(permissionBoundary.isValid).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
{
|
||||||
|
operator: PermissionConditionOperators.$EQ,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
secretPath: { [PermissionConditionOperators.$EQ]: "/print" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
operator: PermissionConditionOperators.$NEQ,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
secretPath: { [PermissionConditionOperators.$NEQ]: "/hello/world" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
operator: PermissionConditionOperators.$IN,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
secretPath: { [PermissionConditionOperators.$IN]: ["/", "/hello"] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
operator: PermissionConditionOperators.$GLOB,
|
||||||
|
childPermission: createMongoAbility([
|
||||||
|
{
|
||||||
|
action: ["create"],
|
||||||
|
subject: "secrets",
|
||||||
|
conditions: {
|
||||||
|
secretPath: { [PermissionConditionOperators.$GLOB]: "/hello**" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
}
|
||||||
|
])("Child $operator falsy cases", ({ childPermission }) => {
|
||||||
|
const permissionBoundary = validatePermissionBoundary(parentPermission, childPermission);
|
||||||
|
expect(permissionBoundary.isValid).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,249 @@
|
|||||||
|
import { MongoAbility } from "@casl/ability";
|
||||||
|
import { MongoQuery } from "@ucast/mongo2js";
|
||||||
|
import picomatch from "picomatch";
|
||||||
|
|
||||||
|
import { PermissionConditionOperators } from "./index";
|
||||||
|
|
||||||
|
type TMissingPermission = {
|
||||||
|
action: string;
|
||||||
|
subject: string;
|
||||||
|
conditions?: MongoQuery;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TPermissionConditionShape = {
|
||||||
|
[PermissionConditionOperators.$EQ]: string;
|
||||||
|
[PermissionConditionOperators.$NEQ]: string;
|
||||||
|
[PermissionConditionOperators.$GLOB]: string;
|
||||||
|
[PermissionConditionOperators.$IN]: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPermissionSetID = (action: string, subject: string) => `${action}:${subject}`;
|
||||||
|
const invertTheOperation = (shouldInvert: boolean, operation: boolean) => (shouldInvert ? !operation : operation);
|
||||||
|
const formatConditionOperator = (condition: TPermissionConditionShape | string) => {
|
||||||
|
return (
|
||||||
|
typeof condition === "string" ? { [PermissionConditionOperators.$EQ]: condition } : condition
|
||||||
|
) as TPermissionConditionShape;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isOperatorsASubset = (parentSet: TPermissionConditionShape, subset: TPermissionConditionShape) => {
|
||||||
|
// we compute each operator against each other in left hand side and right hand side
|
||||||
|
if (subset[PermissionConditionOperators.$EQ] || subset[PermissionConditionOperators.$NEQ]) {
|
||||||
|
const subsetOperatorValue = subset[PermissionConditionOperators.$EQ] || subset[PermissionConditionOperators.$NEQ];
|
||||||
|
const isInverted = Boolean(subset[PermissionConditionOperators.$NEQ]);
|
||||||
|
if (
|
||||||
|
parentSet[PermissionConditionOperators.$EQ] &&
|
||||||
|
invertTheOperation(isInverted, parentSet[PermissionConditionOperators.$EQ] !== subsetOperatorValue)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
parentSet[PermissionConditionOperators.$NEQ] &&
|
||||||
|
invertTheOperation(isInverted, parentSet[PermissionConditionOperators.$NEQ] === subsetOperatorValue)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
parentSet[PermissionConditionOperators.$IN] &&
|
||||||
|
invertTheOperation(isInverted, !parentSet[PermissionConditionOperators.$IN].includes(subsetOperatorValue))
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// ne and glob cannot match each other
|
||||||
|
if (parentSet[PermissionConditionOperators.$GLOB] && isInverted) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
parentSet[PermissionConditionOperators.$GLOB] &&
|
||||||
|
!picomatch.isMatch(subsetOperatorValue, parentSet[PermissionConditionOperators.$GLOB], { strictSlashes: false })
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (subset[PermissionConditionOperators.$IN]) {
|
||||||
|
const subsetOperatorValue = subset[PermissionConditionOperators.$IN];
|
||||||
|
if (
|
||||||
|
parentSet[PermissionConditionOperators.$EQ] &&
|
||||||
|
(subsetOperatorValue.length !== 1 || subsetOperatorValue[0] !== parentSet[PermissionConditionOperators.$EQ])
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
parentSet[PermissionConditionOperators.$NEQ] &&
|
||||||
|
subsetOperatorValue.includes(parentSet[PermissionConditionOperators.$NEQ])
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
parentSet[PermissionConditionOperators.$IN] &&
|
||||||
|
!subsetOperatorValue.every((el) => parentSet[PermissionConditionOperators.$IN].includes(el))
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
parentSet[PermissionConditionOperators.$GLOB] &&
|
||||||
|
!subsetOperatorValue.every((el) =>
|
||||||
|
picomatch.isMatch(el, parentSet[PermissionConditionOperators.$GLOB], {
|
||||||
|
strictSlashes: false
|
||||||
|
})
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (subset[PermissionConditionOperators.$GLOB]) {
|
||||||
|
const subsetOperatorValue = subset[PermissionConditionOperators.$GLOB];
|
||||||
|
const { isGlob } = picomatch.scan(subsetOperatorValue);
|
||||||
|
// if it's glob, all other fixed operators would make this superset because glob is powerful. like eq
|
||||||
|
// example: $in [dev, prod] => glob: dev** could mean anything starting with dev: thus is bigger
|
||||||
|
if (
|
||||||
|
isGlob &&
|
||||||
|
Object.keys(parentSet).some(
|
||||||
|
(el) => el !== PermissionConditionOperators.$GLOB && el !== PermissionConditionOperators.$NEQ
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
parentSet[PermissionConditionOperators.$EQ] &&
|
||||||
|
parentSet[PermissionConditionOperators.$EQ] !== subsetOperatorValue
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
parentSet[PermissionConditionOperators.$NEQ] &&
|
||||||
|
picomatch.isMatch(parentSet[PermissionConditionOperators.$NEQ], subsetOperatorValue, {
|
||||||
|
strictSlashes: false
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// if parent set is IN, glob cannot be used for children - It's a bigger scope
|
||||||
|
if (
|
||||||
|
parentSet[PermissionConditionOperators.$IN] &&
|
||||||
|
!parentSet[PermissionConditionOperators.$IN].includes(subsetOperatorValue)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
parentSet[PermissionConditionOperators.$GLOB] &&
|
||||||
|
!picomatch.isMatch(subsetOperatorValue, parentSet[PermissionConditionOperators.$GLOB], {
|
||||||
|
strictSlashes: false
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isSubsetForSamePermissionSubjectAction = (
|
||||||
|
parentSetRules: ReturnType<MongoAbility["possibleRulesFor"]>,
|
||||||
|
subsetRules: ReturnType<MongoAbility["possibleRulesFor"]>,
|
||||||
|
appendToMissingPermission: (condition?: MongoQuery) => void
|
||||||
|
) => {
|
||||||
|
const isMissingConditionInParent = parentSetRules.every((el) => !el.conditions);
|
||||||
|
if (isMissingConditionInParent) return true;
|
||||||
|
|
||||||
|
// all subset rules must pass in comparison to parent rul
|
||||||
|
return subsetRules.every((subsetRule) => {
|
||||||
|
const subsetRuleConditions = subsetRule.conditions as Record<string, TPermissionConditionShape | string>;
|
||||||
|
// compare subset rule with all parent rules
|
||||||
|
const isSubsetOfNonInvertedParentSet = parentSetRules
|
||||||
|
.filter((el) => !el.inverted)
|
||||||
|
.some((parentSetRule) => {
|
||||||
|
// get conditions and iterate
|
||||||
|
const parentSetRuleConditions = parentSetRule?.conditions as Record<string, TPermissionConditionShape | string>;
|
||||||
|
if (!parentSetRuleConditions) return true;
|
||||||
|
return Object.keys(parentSetRuleConditions).every((parentConditionField) => {
|
||||||
|
// if parent condition is missing then it's never a subset
|
||||||
|
if (!subsetRuleConditions?.[parentConditionField]) return false;
|
||||||
|
|
||||||
|
// standardize the conditions plain string operator => $eq function
|
||||||
|
const parentRuleConditionOperators = formatConditionOperator(parentSetRuleConditions[parentConditionField]);
|
||||||
|
const selectedSubsetRuleCondition = subsetRuleConditions?.[parentConditionField];
|
||||||
|
const subsetRuleConditionOperators = formatConditionOperator(selectedSubsetRuleCondition);
|
||||||
|
return isOperatorsASubset(parentRuleConditionOperators, subsetRuleConditionOperators);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const invertedParentSetRules = parentSetRules.filter((el) => el.inverted);
|
||||||
|
const isNotSubsetOfInvertedParentSet = invertedParentSetRules.length
|
||||||
|
? !invertedParentSetRules.some((parentSetRule) => {
|
||||||
|
// get conditions and iterate
|
||||||
|
const parentSetRuleConditions = parentSetRule?.conditions as Record<
|
||||||
|
string,
|
||||||
|
TPermissionConditionShape | string
|
||||||
|
>;
|
||||||
|
if (!parentSetRuleConditions) return true;
|
||||||
|
return Object.keys(parentSetRuleConditions).every((parentConditionField) => {
|
||||||
|
// if parent condition is missing then it's never a subset
|
||||||
|
if (!subsetRuleConditions?.[parentConditionField]) return false;
|
||||||
|
|
||||||
|
// standardize the conditions plain string operator => $eq function
|
||||||
|
const parentRuleConditionOperators = formatConditionOperator(parentSetRuleConditions[parentConditionField]);
|
||||||
|
const selectedSubsetRuleCondition = subsetRuleConditions?.[parentConditionField];
|
||||||
|
const subsetRuleConditionOperators = formatConditionOperator(selectedSubsetRuleCondition);
|
||||||
|
return isOperatorsASubset(parentRuleConditionOperators, subsetRuleConditionOperators);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
: true;
|
||||||
|
const isSubset = isSubsetOfNonInvertedParentSet && isNotSubsetOfInvertedParentSet;
|
||||||
|
if (!isSubset) {
|
||||||
|
appendToMissingPermission(subsetRule.conditions);
|
||||||
|
}
|
||||||
|
return isSubset;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const validatePermissionBoundary = (parentSetPermissions: MongoAbility, subsetPermissions: MongoAbility) => {
|
||||||
|
const checkedPermissionRules = new Set<string>();
|
||||||
|
const missingPermissions: TMissingPermission[] = [];
|
||||||
|
|
||||||
|
subsetPermissions.rules.forEach((subsetPermissionRules) => {
|
||||||
|
const subsetPermissionSubject = subsetPermissionRules.subject.toString();
|
||||||
|
let subsetPermissionActions: string[] = [];
|
||||||
|
|
||||||
|
// actions can be string or string[]
|
||||||
|
if (typeof subsetPermissionRules.action === "string") {
|
||||||
|
subsetPermissionActions.push(subsetPermissionRules.action);
|
||||||
|
} else {
|
||||||
|
subsetPermissionRules.action.forEach((subsetPermissionAction) => {
|
||||||
|
subsetPermissionActions.push(subsetPermissionAction);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// if action is already processed ignore
|
||||||
|
subsetPermissionActions = subsetPermissionActions.filter(
|
||||||
|
(el) => !checkedPermissionRules.has(getPermissionSetID(el, subsetPermissionSubject))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!subsetPermissionActions.length) return;
|
||||||
|
subsetPermissionActions.forEach((subsetPermissionAction) => {
|
||||||
|
const parentSetRulesOfSubset = parentSetPermissions.possibleRulesFor(
|
||||||
|
subsetPermissionAction,
|
||||||
|
subsetPermissionSubject
|
||||||
|
);
|
||||||
|
const nonInveretedOnes = parentSetRulesOfSubset.filter((el) => !el.inverted);
|
||||||
|
if (!nonInveretedOnes.length) {
|
||||||
|
missingPermissions.push({ action: subsetPermissionAction, subject: subsetPermissionSubject });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const subsetRules = subsetPermissions.possibleRulesFor(subsetPermissionAction, subsetPermissionSubject);
|
||||||
|
isSubsetForSamePermissionSubjectAction(parentSetRulesOfSubset, subsetRules, (conditions) => {
|
||||||
|
missingPermissions.push({ action: subsetPermissionAction, subject: subsetPermissionSubject, conditions });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
subsetPermissionActions.forEach((el) =>
|
||||||
|
checkedPermissionRules.add(getPermissionSetID(el, subsetPermissionSubject))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (missingPermissions.length) {
|
||||||
|
return { isValid: false as const, missingPermissions };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { isValid: true };
|
||||||
|
};
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||||
import { buildMongoQueryMatcher, createMongoAbility, MongoAbility } from "@casl/ability";
|
import { buildMongoQueryMatcher } from "@casl/ability";
|
||||||
import { FieldCondition, FieldInstruction, JsInterpreter } from "@ucast/mongo2js";
|
import { FieldCondition, FieldInstruction, JsInterpreter } from "@ucast/mongo2js";
|
||||||
import picomatch from "picomatch";
|
import picomatch from "picomatch";
|
||||||
|
|
||||||
@@ -26,263 +26,3 @@ export enum PermissionConditionOperators {
|
|||||||
$NEQ = "$ne",
|
$NEQ = "$ne",
|
||||||
$GLOB = "$glob"
|
$GLOB = "$glob"
|
||||||
}
|
}
|
||||||
|
|
||||||
type TPermissionConditionShape = {
|
|
||||||
[PermissionConditionOperators.$EQ]: string;
|
|
||||||
[PermissionConditionOperators.$NEQ]: string;
|
|
||||||
[PermissionConditionOperators.$GLOB]: string;
|
|
||||||
[PermissionConditionOperators.$IN]: string[];
|
|
||||||
};
|
|
||||||
|
|
||||||
const getPermissionSetContainerID = (action: string, subject: string) => `${action}:${subject}`;
|
|
||||||
const invertTheOperation = (shouldInvert: boolean, operation: boolean) => (shouldInvert ? !operation : operation);
|
|
||||||
const formatConditionOperator = (condition: TPermissionConditionShape | string) => {
|
|
||||||
return (
|
|
||||||
typeof condition === "string" ? { [PermissionConditionOperators.$EQ]: condition } : condition
|
|
||||||
) as TPermissionConditionShape;
|
|
||||||
};
|
|
||||||
|
|
||||||
const isOperatorsASubset = (parentSet: TPermissionConditionShape, subset: TPermissionConditionShape) => {
|
|
||||||
if (subset[PermissionConditionOperators.$EQ] || subset[PermissionConditionOperators.$NEQ]) {
|
|
||||||
const subsetOperatorValue = subset[PermissionConditionOperators.$EQ] || subset[PermissionConditionOperators.$NEQ];
|
|
||||||
const isInverted = Boolean(subset[PermissionConditionOperators.$NEQ]);
|
|
||||||
if (
|
|
||||||
parentSet[PermissionConditionOperators.$EQ] &&
|
|
||||||
invertTheOperation(isInverted, parentSet[PermissionConditionOperators.$EQ] !== subsetOperatorValue)
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
parentSet[PermissionConditionOperators.$NEQ] &&
|
|
||||||
invertTheOperation(isInverted, parentSet[PermissionConditionOperators.$NEQ] === subsetOperatorValue)
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
parentSet[PermissionConditionOperators.$IN] &&
|
|
||||||
invertTheOperation(isInverted, !parentSet[PermissionConditionOperators.$IN].includes(subsetOperatorValue))
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
parentSet[PermissionConditionOperators.$GLOB] &&
|
|
||||||
invertTheOperation(
|
|
||||||
isInverted,
|
|
||||||
!picomatch.isMatch(subsetOperatorValue, parentSet[PermissionConditionOperators.$GLOB], { strictSlashes: false })
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (subset[PermissionConditionOperators.$IN]) {
|
|
||||||
const subsetOperatorValue = subset[PermissionConditionOperators.$IN];
|
|
||||||
if (
|
|
||||||
parentSet[PermissionConditionOperators.$EQ] &&
|
|
||||||
(subsetOperatorValue.length !== 1 || subsetOperatorValue[0] !== parentSet[PermissionConditionOperators.$EQ])
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
parentSet[PermissionConditionOperators.$NEQ] &&
|
|
||||||
!subsetOperatorValue.includes(parentSet[PermissionConditionOperators.$NEQ])
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
parentSet[PermissionConditionOperators.$IN] &&
|
|
||||||
!subsetOperatorValue.every((el) => parentSet[PermissionConditionOperators.$IN].includes(el))
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
parentSet[PermissionConditionOperators.$GLOB] &&
|
|
||||||
!subsetOperatorValue.every((el) =>
|
|
||||||
picomatch.isMatch(el, parentSet[PermissionConditionOperators.$GLOB], {
|
|
||||||
strictSlashes: false
|
|
||||||
})
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (subset[PermissionConditionOperators.$GLOB]) {
|
|
||||||
const subsetOperatorValue = subset[PermissionConditionOperators.$GLOB];
|
|
||||||
const { isGlob } = picomatch.scan(subsetOperatorValue);
|
|
||||||
// if it's glob, all other fixed operators would make this superset because glob is powerful. like eq
|
|
||||||
// example: $in [dev, prod] => glob: dev** could mean anything starting with dev: thus is bigger
|
|
||||||
if (
|
|
||||||
isGlob &&
|
|
||||||
Object.keys(parentSet).some(
|
|
||||||
(el) => el !== PermissionConditionOperators.$GLOB && el !== PermissionConditionOperators.$NEQ
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
parentSet[PermissionConditionOperators.$EQ] &&
|
|
||||||
parentSet[PermissionConditionOperators.$EQ] !== subsetOperatorValue
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
parentSet[PermissionConditionOperators.$NEQ] &&
|
|
||||||
picomatch.isMatch(parentSet[PermissionConditionOperators.$NEQ], subsetOperatorValue, {
|
|
||||||
strictSlashes: false
|
|
||||||
})
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// if parent set is IN, glob cannot be used for children - It's a bigger scope
|
|
||||||
if (
|
|
||||||
parentSet[PermissionConditionOperators.$IN] &&
|
|
||||||
!parentSet[PermissionConditionOperators.$IN].includes(subsetOperatorValue)
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
parentSet[PermissionConditionOperators.$GLOB] &&
|
|
||||||
!picomatch.isMatch(subsetOperatorValue, parentSet[PermissionConditionOperators.$GLOB], {
|
|
||||||
strictSlashes: false
|
|
||||||
})
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const isSubsetForSamePermissionSubjectAction = (
|
|
||||||
// [{action, subject,conditions{env: dev}}]
|
|
||||||
parentSetRules: ReturnType<MongoAbility["possibleRulesFor"]>,
|
|
||||||
// [{action, subject,conditions{env: prod}}, {action, subject,conditions{env: dev,secretPath: "/"}]
|
|
||||||
subsetRules: ReturnType<MongoAbility["possibleRulesFor"]>
|
|
||||||
) => {
|
|
||||||
const isMissingConditionInParent = parentSetRules.every((el) => !el.conditions);
|
|
||||||
if (isMissingConditionInParent) return true;
|
|
||||||
|
|
||||||
// all subset rules must pass in comparison to parent rul
|
|
||||||
return subsetRules.every((subsetRule) => {
|
|
||||||
const subsetRuleConditions = subsetRule.conditions as Record<string, TPermissionConditionShape | string>;
|
|
||||||
|
|
||||||
// compare subset rule with all parent rules
|
|
||||||
const isSubsetOfNonInvertedParentSet = parentSetRules
|
|
||||||
.filter((el) => !el.inverted)
|
|
||||||
.some((parentSetRule) => {
|
|
||||||
// get conditions and iterate
|
|
||||||
const parentSetRuleConditions = parentSetRule?.conditions as Record<string, TPermissionConditionShape | string>;
|
|
||||||
if (!parentSetRuleConditions) return true;
|
|
||||||
return Object.keys(parentSetRuleConditions).every((parentConditionField) => {
|
|
||||||
// if parent condition is missing then it's never a subset
|
|
||||||
if (!subsetRuleConditions?.[parentConditionField]) return false;
|
|
||||||
|
|
||||||
// standardize the conditions plain string operator => $eq function
|
|
||||||
const parentRuleConditionOperators = formatConditionOperator(parentSetRuleConditions[parentConditionField]);
|
|
||||||
const selectedSubsetRuleCondition = subsetRuleConditions?.[parentConditionField];
|
|
||||||
const subsetRuleConditionOperators = formatConditionOperator(selectedSubsetRuleCondition);
|
|
||||||
return isOperatorsASubset(parentRuleConditionOperators, subsetRuleConditionOperators);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const invertedParentSetRules = parentSetRules.filter((el) => el.inverted);
|
|
||||||
const isNotSubsetOfInvertedParentSet = invertedParentSetRules.length
|
|
||||||
? !invertedParentSetRules.some((parentSetRule) => {
|
|
||||||
// get conditions and iterate
|
|
||||||
const parentSetRuleConditions = parentSetRule?.conditions as Record<
|
|
||||||
string,
|
|
||||||
TPermissionConditionShape | string
|
|
||||||
>;
|
|
||||||
if (!parentSetRuleConditions) return true;
|
|
||||||
return Object.keys(parentSetRuleConditions).every((parentConditionField) => {
|
|
||||||
// if parent condition is missing then it's never a subset
|
|
||||||
if (!subsetRuleConditions?.[parentConditionField]) return false;
|
|
||||||
|
|
||||||
// standardize the conditions plain string operator => $eq function
|
|
||||||
const parentRuleConditionOperators = formatConditionOperator(parentSetRuleConditions[parentConditionField]);
|
|
||||||
const selectedSubsetRuleCondition = subsetRuleConditions?.[parentConditionField];
|
|
||||||
const subsetRuleConditionOperators = formatConditionOperator(selectedSubsetRuleCondition);
|
|
||||||
return isOperatorsASubset(parentRuleConditionOperators, subsetRuleConditionOperators);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
: true;
|
|
||||||
return isSubsetOfNonInvertedParentSet && isNotSubsetOfInvertedParentSet;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Compares two sets of permissions to determine if the first set is at least as privileged as the second set.
|
|
||||||
* The function checks if all permissions in the second set are contained within the first set and if the first set has equal or more permissions.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
export const isAtLeastAsPrivileged = (parentSetPermissions: MongoAbility, subsetPermissions: MongoAbility) => {
|
|
||||||
const checkedPermissionRules = new Set<string>();
|
|
||||||
for (const subsetPermissionRules of subsetPermissions.rules) {
|
|
||||||
const subsetPermissionSubject = subsetPermissionRules.subject.toString();
|
|
||||||
let subsetPermissionActions: string[] = [];
|
|
||||||
|
|
||||||
if (typeof subsetPermissionRules.action === "string") {
|
|
||||||
subsetPermissionActions.push(subsetPermissionRules.action);
|
|
||||||
} else {
|
|
||||||
subsetPermissionRules.action.forEach((subsetPermissionAction) => {
|
|
||||||
subsetPermissionActions.push(subsetPermissionAction);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
subsetPermissionActions = subsetPermissionActions.filter(
|
|
||||||
(el) => !checkedPermissionRules.has(getPermissionSetContainerID(el, subsetPermissionSubject))
|
|
||||||
);
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-continue
|
|
||||||
if (!subsetPermissionActions.length) continue;
|
|
||||||
// eslint-disable-next-line no-unreachable-loop
|
|
||||||
for (const subsetPermissionAction of subsetPermissionActions) {
|
|
||||||
const parentSetRulesOfSubset = parentSetPermissions.possibleRulesFor(
|
|
||||||
subsetPermissionAction,
|
|
||||||
subsetPermissionSubject
|
|
||||||
);
|
|
||||||
const nonInveretedOnes = parentSetRulesOfSubset.filter((el) => !el.inverted);
|
|
||||||
if (!nonInveretedOnes.length) return false;
|
|
||||||
|
|
||||||
const subsetRules = subsetPermissions.possibleRulesFor(subsetPermissionAction, subsetPermissionSubject);
|
|
||||||
const isSubset = isSubsetForSamePermissionSubjectAction(parentSetRulesOfSubset, subsetRules);
|
|
||||||
if (!isSubset) return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
subsetPermissionActions.forEach((el) =>
|
|
||||||
checkedPermissionRules.add(getPermissionSetContainerID(el, subsetPermissionSubject))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const superset = createMongoAbility([
|
|
||||||
{
|
|
||||||
action: ["create", "edit", "delete", "read"],
|
|
||||||
subject: "secrets",
|
|
||||||
conditions: {
|
|
||||||
environment: { [PermissionConditionOperators.$EQ]: "dev" }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
action: "read",
|
|
||||||
subject: "secrets",
|
|
||||||
inverted: true,
|
|
||||||
conditions: {
|
|
||||||
environment: { [PermissionConditionOperators.$EQ]: "dev" },
|
|
||||||
secretPath: { [PermissionConditionOperators.$GLOB]: "/hello" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
|
|
||||||
const subset = createMongoAbility([
|
|
||||||
{
|
|
||||||
action: "edit",
|
|
||||||
subject: "secrets",
|
|
||||||
conditions: {
|
|
||||||
environment: { [PermissionConditionOperators.$EQ]: "dev" },
|
|
||||||
secretPath: { [PermissionConditionOperators.$EQ]: "/hello" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
|
|
||||||
console.log(isAtLeastAsPrivileged(superset, subset));
|
|
||||||
|
|||||||
@@ -52,10 +52,18 @@ export class ForbiddenRequestError extends Error {
|
|||||||
|
|
||||||
error: unknown;
|
error: unknown;
|
||||||
|
|
||||||
constructor({ name, error, message }: { message?: string; name?: string; error?: unknown } = {}) {
|
details?: unknown;
|
||||||
|
|
||||||
|
constructor({
|
||||||
|
name,
|
||||||
|
error,
|
||||||
|
message,
|
||||||
|
details
|
||||||
|
}: { message?: string; name?: string; error?: unknown; details?: unknown } = {}) {
|
||||||
super(message ?? "You are not allowed to access this resource");
|
super(message ?? "You are not allowed to access this resource");
|
||||||
this.name = name || "ForbiddenError";
|
this.name = name || "ForbiddenError";
|
||||||
this.error = error;
|
this.error = error;
|
||||||
|
this.details = details;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -122,7 +122,8 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider
|
|||||||
reqId: req.id,
|
reqId: req.id,
|
||||||
statusCode: HttpStatusCodes.Forbidden,
|
statusCode: HttpStatusCodes.Forbidden,
|
||||||
message: error.message,
|
message: error.message,
|
||||||
error: error.name
|
error: error.name,
|
||||||
|
details: error?.details
|
||||||
});
|
});
|
||||||
} else if (error instanceof RateLimitError) {
|
} else if (error instanceof RateLimitError) {
|
||||||
void res.status(HttpStatusCodes.TooManyRequests).send({
|
void res.status(HttpStatusCodes.TooManyRequests).send({
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import ms from "ms";
|
|||||||
import { ActionProjectType, ProjectMembershipRole, SecretKeyEncoding, TGroups } from "@app/db/schemas";
|
import { ActionProjectType, ProjectMembershipRole, SecretKeyEncoding, TGroups } 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 { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
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, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
@@ -102,11 +102,13 @@ export const groupProjectServiceFactory = ({
|
|||||||
project.id
|
project.id
|
||||||
);
|
);
|
||||||
|
|
||||||
const hasRequiredPrivileges = isAtLeastAsPrivileged(permission, rolePermission);
|
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
|
||||||
|
if (!permissionBoundary.isValid)
|
||||||
if (!hasRequiredPrivileges) {
|
throw new ForbiddenRequestError({
|
||||||
throw new ForbiddenRequestError({ message: "Failed to assign group to a more privileged role" });
|
name: "PermissionBoundaryError",
|
||||||
}
|
message: "Failed to assign group to a more privileged role",
|
||||||
|
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// validate custom roles input
|
// validate custom roles input
|
||||||
@@ -267,12 +269,13 @@ export const groupProjectServiceFactory = ({
|
|||||||
requestedRoleChange,
|
requestedRoleChange,
|
||||||
project.id
|
project.id
|
||||||
);
|
);
|
||||||
|
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
|
||||||
const hasRequiredPrivileges = isAtLeastAsPrivileged(permission, rolePermission);
|
if (!permissionBoundary.isValid)
|
||||||
|
throw new ForbiddenRequestError({
|
||||||
if (!hasRequiredPrivileges) {
|
name: "PermissionBoundaryError",
|
||||||
throw new ForbiddenRequestError({ message: "Failed to assign group to a more privileged role" });
|
message: "Failed to assign group to a more privileged role",
|
||||||
}
|
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// validate custom roles input
|
// validate custom roles input
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ 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 { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
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, UnauthorizedError } from "@app/lib/errors";
|
||||||
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
||||||
@@ -339,9 +339,12 @@ export const identityAwsAuthServiceFactory = ({
|
|||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!isAtLeastAsPrivileged(permission, rolePermission))
|
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
|
||||||
|
if (!permissionBoundary.isValid)
|
||||||
throw new ForbiddenRequestError({
|
throw new ForbiddenRequestError({
|
||||||
message: "Failed to revoke aws auth of identity with more privileged role"
|
name: "PermissionBoundaryError",
|
||||||
|
message: "Failed to revoke aws auth of identity with more privileged role",
|
||||||
|
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||||
});
|
});
|
||||||
|
|
||||||
const revokedIdentityAwsAuth = await identityAwsAuthDAL.transaction(async (tx) => {
|
const revokedIdentityAwsAuth = await identityAwsAuthDAL.transaction(async (tx) => {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ 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 { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
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, UnauthorizedError } from "@app/lib/errors";
|
||||||
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
||||||
@@ -312,9 +312,12 @@ export const identityAzureAuthServiceFactory = ({
|
|||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
if (!isAtLeastAsPrivileged(permission, rolePermission))
|
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
|
||||||
|
if (!permissionBoundary.isValid)
|
||||||
throw new ForbiddenRequestError({
|
throw new ForbiddenRequestError({
|
||||||
message: "Failed to revoke azure auth of identity with more privileged role"
|
name: "PermissionBoundaryError",
|
||||||
|
message: "Failed to revoke azure auth of identity with more privileged role",
|
||||||
|
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||||
});
|
});
|
||||||
|
|
||||||
const revokedIdentityAzureAuth = await identityAzureAuthDAL.transaction(async (tx) => {
|
const revokedIdentityAzureAuth = await identityAzureAuthDAL.transaction(async (tx) => {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ 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 { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
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, UnauthorizedError } from "@app/lib/errors";
|
||||||
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
||||||
@@ -358,9 +358,12 @@ export const identityGcpAuthServiceFactory = ({
|
|||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
if (!isAtLeastAsPrivileged(permission, rolePermission))
|
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
|
||||||
|
if (!permissionBoundary.isValid)
|
||||||
throw new ForbiddenRequestError({
|
throw new ForbiddenRequestError({
|
||||||
message: "Failed to revoke gcp auth of identity with more privileged role"
|
name: "PermissionBoundaryError",
|
||||||
|
message: "Failed to revoke gcp auth of identity with more privileged role",
|
||||||
|
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||||
});
|
});
|
||||||
|
|
||||||
const revokedIdentityGcpAuth = await identityGcpAuthDAL.transaction(async (tx) => {
|
const revokedIdentityGcpAuth = await identityGcpAuthDAL.transaction(async (tx) => {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ 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 { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
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, UnauthorizedError } from "@app/lib/errors";
|
||||||
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
||||||
@@ -508,11 +508,13 @@ export const identityJwtAuthServiceFactory = ({
|
|||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!isAtLeastAsPrivileged(permission, rolePermission)) {
|
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
|
||||||
|
if (!permissionBoundary.isValid)
|
||||||
throw new ForbiddenRequestError({
|
throw new ForbiddenRequestError({
|
||||||
message: "Failed to revoke JWT auth of identity with more privileged role"
|
name: "PermissionBoundaryError",
|
||||||
|
message: "Failed to revoke jwt auth of identity with more privileged role",
|
||||||
|
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
const revokedIdentityJwtAuth = await identityJwtAuthDAL.transaction(async (tx) => {
|
const revokedIdentityJwtAuth = await identityJwtAuthDAL.transaction(async (tx) => {
|
||||||
const deletedJwtAuth = await identityJwtAuthDAL.delete({ identityId }, tx);
|
const deletedJwtAuth = await identityJwtAuthDAL.delete({ identityId }, tx);
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { IdentityAuthMethod, SecretKeyEncoding, TIdentityKubernetesAuthsUpdate }
|
|||||||
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 { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import {
|
import {
|
||||||
decryptSymmetric,
|
decryptSymmetric,
|
||||||
@@ -616,9 +616,12 @@ export const identityKubernetesAuthServiceFactory = ({
|
|||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
if (!isAtLeastAsPrivileged(permission, rolePermission))
|
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
|
||||||
|
if (!permissionBoundary.isValid)
|
||||||
throw new ForbiddenRequestError({
|
throw new ForbiddenRequestError({
|
||||||
message: "Failed to revoke kubernetes auth of identity with more privileged role"
|
name: "PermissionBoundaryError",
|
||||||
|
message: "Failed to revoke kubernetes auth of identity with more privileged role",
|
||||||
|
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||||
});
|
});
|
||||||
|
|
||||||
const revokedIdentityKubernetesAuth = await identityKubernetesAuthDAL.transaction(async (tx) => {
|
const revokedIdentityKubernetesAuth = await identityKubernetesAuthDAL.transaction(async (tx) => {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { IdentityAuthMethod, SecretKeyEncoding, TIdentityOidcAuthsUpdate } from
|
|||||||
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 { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
|
||||||
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 {
|
import {
|
||||||
@@ -531,11 +531,13 @@ export const identityOidcAuthServiceFactory = ({
|
|||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!isAtLeastAsPrivileged(permission, rolePermission)) {
|
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
|
||||||
|
if (!permissionBoundary.isValid)
|
||||||
throw new ForbiddenRequestError({
|
throw new ForbiddenRequestError({
|
||||||
message: "Failed to revoke OIDC auth of identity with more privileged role"
|
name: "PermissionBoundaryError",
|
||||||
|
message: "Failed to revoke oidc auth of identity with more privileged role",
|
||||||
|
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
const revokedIdentityOidcAuth = await identityOidcAuthDAL.transaction(async (tx) => {
|
const revokedIdentityOidcAuth = await identityOidcAuthDAL.transaction(async (tx) => {
|
||||||
const deletedOidcAuth = await identityOidcAuthDAL.delete({ identityId }, tx);
|
const deletedOidcAuth = await identityOidcAuthDAL.delete({ identityId }, tx);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import ms from "ms";
|
|||||||
import { ActionProjectType, ProjectMembershipRole } from "@app/db/schemas";
|
import { ActionProjectType, ProjectMembershipRole } 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 { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
|
||||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
import { groupBy } from "@app/lib/fn";
|
import { groupBy } from "@app/lib/fn";
|
||||||
|
|
||||||
@@ -91,11 +91,13 @@ export const identityProjectServiceFactory = ({
|
|||||||
projectId
|
projectId
|
||||||
);
|
);
|
||||||
|
|
||||||
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, rolePermission);
|
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
|
||||||
|
if (!permissionBoundary.isValid)
|
||||||
if (!hasRequiredPriviledges) {
|
throw new ForbiddenRequestError({
|
||||||
throw new ForbiddenRequestError({ message: "Failed to change to a more privileged role" });
|
name: "PermissionBoundaryError",
|
||||||
}
|
message: "Failed to change to a more privileged role",
|
||||||
|
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// validate custom roles input
|
// validate custom roles input
|
||||||
@@ -185,9 +187,13 @@ export const identityProjectServiceFactory = ({
|
|||||||
projectId
|
projectId
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!isAtLeastAsPrivileged(permission, rolePermission)) {
|
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
|
||||||
throw new ForbiddenRequestError({ message: "Failed to change to a more privileged role" });
|
if (!permissionBoundary.isValid)
|
||||||
}
|
throw new ForbiddenRequestError({
|
||||||
|
name: "PermissionBoundaryError",
|
||||||
|
message: "Failed to change to a more privileged role",
|
||||||
|
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// validate custom roles input
|
// validate custom roles input
|
||||||
@@ -277,8 +283,13 @@ export const identityProjectServiceFactory = ({
|
|||||||
actorOrgId,
|
actorOrgId,
|
||||||
actionProjectType: ActionProjectType.Any
|
actionProjectType: ActionProjectType.Any
|
||||||
});
|
});
|
||||||
if (!isAtLeastAsPrivileged(permission, identityRolePermission))
|
const permissionBoundary = validatePermissionBoundary(permission, identityRolePermission);
|
||||||
throw new ForbiddenRequestError({ message: "Failed to delete more privileged identity" });
|
if (!permissionBoundary.isValid)
|
||||||
|
throw new ForbiddenRequestError({
|
||||||
|
name: "PermissionBoundaryError",
|
||||||
|
message: "Failed to delete 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;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ 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 { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
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 } from "@app/lib/errors";
|
||||||
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
||||||
@@ -245,11 +245,13 @@ export const identityTokenAuthServiceFactory = ({
|
|||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!isAtLeastAsPrivileged(permission, rolePermission)) {
|
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
|
||||||
|
if (!permissionBoundary.isValid)
|
||||||
throw new ForbiddenRequestError({
|
throw new ForbiddenRequestError({
|
||||||
message: "Failed to revoke Token Auth of identity with more privileged role"
|
name: "PermissionBoundaryError",
|
||||||
|
message: "Failed to revoke token auth of identity with more privileged role",
|
||||||
|
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
const revokedIdentityTokenAuth = await identityTokenAuthDAL.transaction(async (tx) => {
|
const revokedIdentityTokenAuth = await identityTokenAuthDAL.transaction(async (tx) => {
|
||||||
const deletedTokenAuth = await identityTokenAuthDAL.delete({ identityId }, tx);
|
const deletedTokenAuth = await identityTokenAuthDAL.delete({ identityId }, tx);
|
||||||
@@ -295,10 +297,12 @@ export const identityTokenAuthServiceFactory = ({
|
|||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission);
|
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
|
||||||
if (!hasPriviledge)
|
if (!permissionBoundary.isValid)
|
||||||
throw new ForbiddenRequestError({
|
throw new ForbiddenRequestError({
|
||||||
message: "Failed to create token for identity with more privileged role"
|
name: "PermissionBoundaryError",
|
||||||
|
message: "Failed to create token for identity with more privileged role",
|
||||||
|
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||||
});
|
});
|
||||||
|
|
||||||
const identityTokenAuth = await identityTokenAuthDAL.findOne({ identityId });
|
const identityTokenAuth = await identityTokenAuthDAL.findOne({ identityId });
|
||||||
@@ -415,10 +419,12 @@ export const identityTokenAuthServiceFactory = ({
|
|||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission);
|
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
|
||||||
if (!hasPriviledge)
|
if (!permissionBoundary.isValid)
|
||||||
throw new ForbiddenRequestError({
|
throw new ForbiddenRequestError({
|
||||||
message: "Failed to update token for identity with more privileged role"
|
name: "PermissionBoundaryError",
|
||||||
|
message: "Failed to update token for identity with more privileged role",
|
||||||
|
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||||
});
|
});
|
||||||
|
|
||||||
const [token] = await identityAccessTokenDAL.update(
|
const [token] = await identityAccessTokenDAL.update(
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ 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 { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
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, UnauthorizedError } from "@app/lib/errors";
|
||||||
import { checkIPAgainstBlocklist, extractIPDetails, isValidIpOrCidr, TIp } from "@app/lib/ip";
|
import { checkIPAgainstBlocklist, extractIPDetails, isValidIpOrCidr, TIp } from "@app/lib/ip";
|
||||||
@@ -367,9 +367,12 @@ export const identityUaServiceFactory = ({
|
|||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
if (!isAtLeastAsPrivileged(permission, rolePermission))
|
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
|
||||||
|
if (!permissionBoundary.isValid)
|
||||||
throw new ForbiddenRequestError({
|
throw new ForbiddenRequestError({
|
||||||
message: "Failed to revoke universal auth of identity with more privileged role"
|
name: "PermissionBoundaryError",
|
||||||
|
message: "Failed to revoke universal auth of identity with more privileged role",
|
||||||
|
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||||
});
|
});
|
||||||
|
|
||||||
const revokedIdentityUniversalAuth = await identityUaDAL.transaction(async (tx) => {
|
const revokedIdentityUniversalAuth = await identityUaDAL.transaction(async (tx) => {
|
||||||
@@ -414,10 +417,12 @@ export const identityUaServiceFactory = ({
|
|||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission);
|
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
|
||||||
if (!hasPriviledge)
|
if (!permissionBoundary.isValid)
|
||||||
throw new ForbiddenRequestError({
|
throw new ForbiddenRequestError({
|
||||||
message: "Failed to add identity to project with more privileged role"
|
name: "PermissionBoundaryError",
|
||||||
|
message: "Failed to add identity to project with more privileged role",
|
||||||
|
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||||
});
|
});
|
||||||
|
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
@@ -475,9 +480,12 @@ export const identityUaServiceFactory = ({
|
|||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!isAtLeastAsPrivileged(permission, rolePermission))
|
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
|
||||||
|
if (!permissionBoundary.isValid)
|
||||||
throw new ForbiddenRequestError({
|
throw new ForbiddenRequestError({
|
||||||
message: "Failed to add identity to project with more privileged role"
|
name: "PermissionBoundaryError",
|
||||||
|
message: "Failed to get identity with more privileged role",
|
||||||
|
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||||
});
|
});
|
||||||
|
|
||||||
const identityUniversalAuth = await identityUaDAL.findOne({
|
const identityUniversalAuth = await identityUaDAL.findOne({
|
||||||
@@ -524,9 +532,12 @@ export const identityUaServiceFactory = ({
|
|||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
if (!isAtLeastAsPrivileged(permission, rolePermission))
|
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
|
||||||
|
if (!permissionBoundary.isValid)
|
||||||
throw new ForbiddenRequestError({
|
throw new ForbiddenRequestError({
|
||||||
message: "Failed to read identity client secret of project with more privileged role"
|
name: "PermissionBoundaryError",
|
||||||
|
message: "Failed to read identity client secret of project with more privileged role",
|
||||||
|
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||||
});
|
});
|
||||||
|
|
||||||
const clientSecret = await identityUaClientSecretDAL.findById(clientSecretId);
|
const clientSecret = await identityUaClientSecretDAL.findById(clientSecretId);
|
||||||
@@ -566,10 +577,12 @@ export const identityUaServiceFactory = ({
|
|||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
|
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
|
||||||
if (!isAtLeastAsPrivileged(permission, rolePermission))
|
if (!permissionBoundary.isValid)
|
||||||
throw new ForbiddenRequestError({
|
throw new ForbiddenRequestError({
|
||||||
message: "Failed to revoke identity client secret with more privileged role"
|
name: "PermissionBoundaryError",
|
||||||
|
message: "Failed to revoke identity client secret with more privileged role",
|
||||||
|
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||||
});
|
});
|
||||||
|
|
||||||
const clientSecret = await identityUaClientSecretDAL.updateById(clientSecretId, {
|
const clientSecret = await identityUaClientSecretDAL.updateById(clientSecretId, {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ 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 { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
|
||||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } 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";
|
||||||
|
|
||||||
@@ -58,9 +58,13 @@ export const identityServiceFactory = ({
|
|||||||
orgId
|
orgId
|
||||||
);
|
);
|
||||||
const isCustomRole = Boolean(customRole);
|
const isCustomRole = Boolean(customRole);
|
||||||
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, rolePermission);
|
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
|
||||||
if (!hasRequiredPriviledges)
|
if (!permissionBoundary.isValid)
|
||||||
throw new ForbiddenRequestError({ message: "Failed to create a more privileged identity" });
|
throw new ForbiddenRequestError({
|
||||||
|
name: "PermissionBoundaryError",
|
||||||
|
message: "Failed to create a more privileged identity",
|
||||||
|
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||||
|
});
|
||||||
|
|
||||||
const plan = await licenseService.getPlan(orgId);
|
const plan = await licenseService.getPlan(orgId);
|
||||||
|
|
||||||
@@ -129,9 +133,13 @@ export const identityServiceFactory = ({
|
|||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, identityRolePermission);
|
const permissionBoundary = validatePermissionBoundary(permission, identityRolePermission);
|
||||||
if (!hasRequiredPriviledges)
|
if (!permissionBoundary.isValid)
|
||||||
throw new ForbiddenRequestError({ message: "Failed to delete more privileged identity" });
|
throw new ForbiddenRequestError({
|
||||||
|
name: "PermissionBoundaryError",
|
||||||
|
message: "Failed to delete a more privileged identity",
|
||||||
|
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||||
|
});
|
||||||
|
|
||||||
let customRole: TOrgRoles | undefined;
|
let customRole: TOrgRoles | undefined;
|
||||||
if (role) {
|
if (role) {
|
||||||
@@ -141,9 +149,13 @@ export const identityServiceFactory = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const isCustomRole = Boolean(customOrgRole);
|
const isCustomRole = Boolean(customOrgRole);
|
||||||
const hasRequiredNewRolePermission = isAtLeastAsPrivileged(permission, rolePermission);
|
const appliedRolePermissionBoundary = validatePermissionBoundary(permission, rolePermission);
|
||||||
if (!hasRequiredNewRolePermission)
|
if (!appliedRolePermissionBoundary.isValid)
|
||||||
throw new ForbiddenRequestError({ message: "Failed to create a more privileged identity" });
|
throw new ForbiddenRequestError({
|
||||||
|
name: "PermissionBoundaryError",
|
||||||
|
message: "Failed to create a more privileged identity",
|
||||||
|
details: { missingPermissions: appliedRolePermissionBoundary.missingPermissions }
|
||||||
|
});
|
||||||
if (isCustomRole) customRole = customOrgRole;
|
if (isCustomRole) customRole = customOrgRole;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,9 +228,13 @@ export const identityServiceFactory = ({
|
|||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, identityRolePermission);
|
const permissionBoundary = validatePermissionBoundary(permission, identityRolePermission);
|
||||||
if (!hasRequiredPriviledges)
|
if (!permissionBoundary.isValid)
|
||||||
throw new ForbiddenRequestError({ message: "Failed to delete more privileged identity" });
|
throw new ForbiddenRequestError({
|
||||||
|
name: "PermissionBoundaryError",
|
||||||
|
message: "Failed to delete more privileged user",
|
||||||
|
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||||
|
});
|
||||||
|
|
||||||
const deletedIdentity = await identityDAL.deleteById(id);
|
const deletedIdentity = await identityDAL.deleteById(id);
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { TLicenseServiceFactory } from "@app/ee/services/license/license-service
|
|||||||
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 { ProjectPermissionActions, 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 { isAtLeastAsPrivileged } from "@app/lib/casl";
|
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 } from "@app/lib/errors";
|
||||||
import { groupBy } from "@app/lib/fn";
|
import { groupBy } from "@app/lib/fn";
|
||||||
@@ -274,13 +274,13 @@ export const projectMembershipServiceFactory = ({
|
|||||||
projectId
|
projectId
|
||||||
);
|
);
|
||||||
|
|
||||||
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, rolePermission);
|
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
|
||||||
|
if (!permissionBoundary.isValid)
|
||||||
if (!hasRequiredPriviledges) {
|
|
||||||
throw new ForbiddenRequestError({
|
throw new ForbiddenRequestError({
|
||||||
message: `Failed to change to a more privileged role ${requestedRoleChange}`
|
name: "PermissionBoundaryError",
|
||||||
|
message: `Failed to change to a more privileged role ${requestedRoleChange}`,
|
||||||
|
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// validate custom roles input
|
// validate custom roles input
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import path from "path";
|
||||||
|
import { defineConfig } from "vitest/config";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
test: {
|
||||||
|
globals: true,
|
||||||
|
env: {
|
||||||
|
NODE_ENV: "test"
|
||||||
|
},
|
||||||
|
include: ["./src/**/*.test.ts"]
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
"@app": path.resolve(__dirname, "./src")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user