feat: completed migration of permission v1 to v2. Pending intense testing

This commit is contained in:
=
2024-10-18 00:33:36 +05:30
parent 7cd83ad945
commit a9e6c229d0
14 changed files with 278 additions and 81 deletions
@@ -0,0 +1,99 @@
/* eslint-disable no-await-in-loop */
import { unpackRules } from "@casl/ability/extra";
import { Knex } from "knex";
import {
backfillPermissionV1SchemaToV2Schema,
ProjectPermissionSub
} from "@app/ee/services/permission/project-permission";
import { TableName } from "../schemas";
const CHUNK_SIZE = 1000;
export async function up(knex: Knex): Promise<void> {
const hasVersion = await knex.schema.hasColumn(TableName.ProjectRoles, "version");
if (!hasVersion) {
await knex.schema.alterTable(TableName.ProjectRoles, (t) => {
t.integer("version").defaultTo(1).notNullable();
});
const docs = await knex(TableName.ProjectRoles).select("*");
const updatedDocs = docs
.filter((i) => {
const permissionString = JSON.stringify(i.permissions || []);
return (
!permissionString.includes(ProjectPermissionSub.SecretImports) &&
!permissionString.includes(ProjectPermissionSub.DynamicSecrets)
);
})
.map((el) => ({
...el,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore-error this is valid ts
permissions: JSON.stringify(packRules(backfillPermissionV1SchemaToV2Schema(unpackRules(el.permissions))))
}));
if (updatedDocs.length) {
for (let i = 0; i < updatedDocs.length; i += CHUNK_SIZE) {
const chunk = updatedDocs.slice(i, i + CHUNK_SIZE);
await knex(TableName.ProjectRoles).insert(chunk).onConflict("id").merge();
}
}
const identityPrivileges = await knex(TableName.IdentityProjectAdditionalPrivilege).select("*");
const updatedIdentityPrivilegesDocs = identityPrivileges
.filter((i) => {
const permissionString = JSON.stringify(i.permissions || []);
return (
!permissionString.includes(ProjectPermissionSub.SecretImports) &&
!permissionString.includes(ProjectPermissionSub.DynamicSecrets) &&
!permissionString.includes(ProjectPermissionSub.SecretFolders)
);
})
.map((el) => ({
...el,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore-error this is valid ts
permissions: JSON.stringify(packRules(backfillPermissionV1SchemaToV2Schema(unpackRules(el.permissions))))
}));
if (updatedIdentityPrivilegesDocs.length) {
for (let i = 0; i < updatedIdentityPrivilegesDocs.length; i += CHUNK_SIZE) {
const chunk = updatedIdentityPrivilegesDocs.slice(i, i + CHUNK_SIZE);
await knex(TableName.IdentityProjectAdditionalPrivilege).insert(chunk).onConflict("id").merge();
}
}
const userPrivileges = await knex(TableName.ProjectUserAdditionalPrivilege).select("*");
const updatedUserPriviegeDocs = userPrivileges
.filter((i) => {
const permissionString = JSON.stringify(i.permissions || []);
return (
!permissionString.includes(ProjectPermissionSub.SecretImports) &&
!permissionString.includes(ProjectPermissionSub.DynamicSecrets) &&
!permissionString.includes(ProjectPermissionSub.SecretFolders)
);
})
.map((el) => ({
...el,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore-error this is valid ts
permissions: JSON.stringify(packRules(backfillPermissionV1SchemaToV2Schema(unpackRules(el.permissions))))
}));
if (docs.length) {
for (let i = 0; i < updatedUserPriviegeDocs.length; i += CHUNK_SIZE) {
const chunk = updatedUserPriviegeDocs.slice(i, i + CHUNK_SIZE);
await knex(TableName.ProjectUserAdditionalPrivilege).insert(chunk).onConflict("id").merge();
}
}
}
}
export async function down(knex: Knex): Promise<void> {
const hasVersion = await knex.schema.hasColumn(TableName.ProjectRoles, "version");
if (hasVersion) {
await knex.schema.alterTable(TableName.ProjectRoles, (t) => {
t.dropColumn("version");
});
// permission change can be ignored
}
}
@@ -4,6 +4,7 @@ import ms from "ms";
import { z } from "zod"; import { z } from "zod";
import { IdentityProjectAdditionalPrivilegeTemporaryMode } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-types"; import { IdentityProjectAdditionalPrivilegeTemporaryMode } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-types";
import { backfillPermissionV1SchemaToV2Schema } from "@app/ee/services/permission/project-permission";
import { IDENTITY_ADDITIONAL_PRIVILEGE } from "@app/lib/api-docs"; import { IDENTITY_ADDITIONAL_PRIVILEGE } from "@app/lib/api-docs";
import { UnauthorizedError } from "@app/lib/errors"; import { UnauthorizedError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid"; import { alphaNumericNanoId } from "@app/lib/nanoid";
@@ -79,7 +80,9 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
...req.body, ...req.body,
slug: req.body.slug ? slugify(req.body.slug) : slugify(alphaNumericNanoId(12)), slug: req.body.slug ? slugify(req.body.slug) : slugify(alphaNumericNanoId(12)),
isTemporary: false, isTemporary: false,
permissions: JSON.stringify(packRules(permission)) // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore-error this is valid ts
permissions: JSON.stringify(packRules(backfillPermissionV1SchemaToV2Schema(permission)))
}); });
return { privilege }; return { privilege };
} }
@@ -159,7 +162,9 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
...req.body, ...req.body,
slug: req.body.slug ? slugify(req.body.slug) : slugify(alphaNumericNanoId(12)), slug: req.body.slug ? slugify(req.body.slug) : slugify(alphaNumericNanoId(12)),
isTemporary: true, isTemporary: true,
permissions: JSON.stringify(packRules(permission)) // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore-error this is valid ts
permissions: JSON.stringify(packRules(backfillPermissionV1SchemaToV2Schema(permission)))
}); });
return { privilege }; return { privilege };
} }
@@ -244,7 +249,11 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
projectSlug: req.body.projectSlug, projectSlug: req.body.projectSlug,
data: { data: {
...updatedInfo, ...updatedInfo,
permissions: permission ? JSON.stringify(packRules(permission)) : undefined permissions: permission
? // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore-error this is valid ts
JSON.stringify(packRules(backfillPermissionV1SchemaToV2Schema(permission)))
: undefined
} }
}); });
return { privilege }; return { privilege };
@@ -3,7 +3,10 @@ import slugify from "@sindresorhus/slugify";
import { z } from "zod"; import { z } from "zod";
import { ProjectMembershipRole, ProjectMembershipsSchema, ProjectRolesSchema } from "@app/db/schemas"; import { ProjectMembershipRole, ProjectMembershipsSchema, ProjectRolesSchema } from "@app/db/schemas";
import { ProjectPermissionV1Schema } from "@app/ee/services/permission/project-permission"; import {
backfillPermissionV1SchemaToV2Schema,
ProjectPermissionV1Schema
} from "@app/ee/services/permission/project-permission";
import { PROJECT_ROLE } from "@app/lib/api-docs"; import { PROJECT_ROLE } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
@@ -61,7 +64,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
projectSlug: req.params.projectSlug, projectSlug: req.params.projectSlug,
data: { data: {
...req.body, ...req.body,
permissions: JSON.stringify(packRules(req.body.permissions)) permissions: JSON.stringify(packRules(backfillPermissionV1SchemaToV2Schema(req.body.permissions)))
} }
}); });
return { role }; return { role };
@@ -122,7 +125,9 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
roleId: req.params.roleId, roleId: req.params.roleId,
data: { data: {
...req.body, ...req.body,
permissions: req.body.permissions ? JSON.stringify(packRules(req.body.permissions)) : undefined permissions: req.body.permissions
? JSON.stringify(packRules(backfillPermissionV1SchemaToV2Schema(req.body.permissions)))
: undefined
} }
}); });
return { role }; return { role };
@@ -1,13 +1,16 @@
import { packRules } from "@casl/ability/extra";
import slugify from "@sindresorhus/slugify"; import slugify from "@sindresorhus/slugify";
import ms from "ms"; import ms from "ms";
import { z } from "zod"; import { z } from "zod";
import { ProjectUserAdditionalPrivilegeSchema } from "@app/db/schemas"; import { ProjectUserAdditionalPrivilegeSchema } from "@app/db/schemas";
import { backfillPermissionV1SchemaToV2Schema } from "@app/ee/services/permission/project-permission";
import { ProjectUserAdditionalPrivilegeTemporaryMode } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-types"; import { ProjectUserAdditionalPrivilegeTemporaryMode } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-types";
import { PROJECT_USER_ADDITIONAL_PRIVILEGE } from "@app/lib/api-docs"; import { PROJECT_USER_ADDITIONAL_PRIVILEGE } from "@app/lib/api-docs";
import { alphaNumericNanoId } from "@app/lib/nanoid"; import { alphaNumericNanoId } from "@app/lib/nanoid";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { ProjectSpecificPrivilegePermissionSchema } from "@app/server/routes/sanitizedSchemas";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodProvider) => { export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodProvider) => {
@@ -31,7 +34,9 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr
}) })
.optional() .optional()
.describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.slug), .describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.slug),
permissions: z.any().array().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.permissions) permissions: ProjectSpecificPrivilegePermissionSchema.describe(
PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.permissions
)
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -49,7 +54,17 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr
...req.body, ...req.body,
slug: req.body.slug ? slugify(req.body.slug) : slugify(alphaNumericNanoId(12)), slug: req.body.slug ? slugify(req.body.slug) : slugify(alphaNumericNanoId(12)),
isTemporary: false, isTemporary: false,
permissions: JSON.stringify(req.body.permissions) permissions: JSON.stringify(
packRules(
backfillPermissionV1SchemaToV2Schema(
req.body.permissions.actions.map((action) => ({
action,
subject: req.body.permissions.subject,
conditions: req.body.permissions.conditions
}))
)
)
)
}); });
return { privilege }; return { privilege };
} }
@@ -75,7 +90,9 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr
}) })
.optional() .optional()
.describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.slug), .describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.slug),
permissions: z.any().array().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.permissions), permissions: ProjectSpecificPrivilegePermissionSchema.describe(
PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.permissions
),
temporaryMode: z temporaryMode: z
.nativeEnum(ProjectUserAdditionalPrivilegeTemporaryMode) .nativeEnum(ProjectUserAdditionalPrivilegeTemporaryMode)
.describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.temporaryMode), .describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.temporaryMode),
@@ -104,7 +121,17 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr
...req.body, ...req.body,
slug: req.body.slug ? slugify(req.body.slug) : `privilege-${slugify(alphaNumericNanoId(12))}`, slug: req.body.slug ? slugify(req.body.slug) : `privilege-${slugify(alphaNumericNanoId(12))}`,
isTemporary: true, isTemporary: true,
permissions: JSON.stringify(req.body.permissions) permissions: JSON.stringify(
packRules(
backfillPermissionV1SchemaToV2Schema(
req.body.permissions.actions.map((action) => ({
action,
subject: req.body.permissions.subject,
conditions: req.body.permissions.conditions
}))
)
)
)
}); });
return { privilege }; return { privilege };
} }
@@ -131,7 +158,9 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr
message: "Slug must be a valid slug" message: "Slug must be a valid slug"
}) })
.describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.slug), .describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.slug),
permissions: z.any().array().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.permissions), permissions: ProjectSpecificPrivilegePermissionSchema.describe(
PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.permissions
).optional(),
isTemporary: z.boolean().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.isTemporary), isTemporary: z.boolean().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.isTemporary),
temporaryMode: z temporaryMode: z
.nativeEnum(ProjectUserAdditionalPrivilegeTemporaryMode) .nativeEnum(ProjectUserAdditionalPrivilegeTemporaryMode)
@@ -160,7 +189,19 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
...req.body, ...req.body,
permissions: req.body.permissions ? JSON.stringify(req.body.permissions) : undefined, permissions: req.body.permissions
? JSON.stringify(
packRules(
backfillPermissionV1SchemaToV2Schema(
req.body.permissions.actions.map((action) => ({
action,
subject: req.body.permissions!.subject,
conditions: req.body.permissions!.conditions
}))
)
)
)
: undefined,
privilegeId: req.params.privilegeId privilegeId: req.params.privilegeId
}); });
return { privilege }; return { privilege };
@@ -1,10 +1,10 @@
import { ForbiddenError, MongoAbility, RawRuleOf } from "@casl/ability"; import { ForbiddenError, MongoAbility, RawRuleOf } from "@casl/ability";
import { PackRule, unpackRules } from "@casl/ability/extra"; import { PackRule, unpackRules } from "@casl/ability/extra";
import ms from "ms"; import ms from "ms";
import { z } from "zod";
import { isAtLeastAsPrivileged } from "@app/lib/casl"; import { isAtLeastAsPrivileged } from "@app/lib/casl";
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 { ActorType } from "@app/services/auth/auth-type"; import { ActorType } from "@app/services/auth/auth-type";
import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal"; import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
@@ -33,15 +33,6 @@ export type TIdentityProjectAdditionalPrivilegeServiceFactory = ReturnType<
>; >;
// TODO(akhilmhdh): move this to more centralized // TODO(akhilmhdh): move this to more centralized
export const UnpackedPermissionSchema = z.object({
subject: z
.union([z.string().min(1), z.string().array()])
.transform((el) => (typeof el !== "string" ? el[0] : el))
.optional(),
action: z.union([z.string().min(1), z.string().array()]).transform((el) => (typeof el === "string" ? [el] : el)),
conditions: z.unknown().optional(),
inverted: z.boolean().optional()
});
const unpackPermissions = (permissions: unknown) => const unpackPermissions = (permissions: unknown) =>
UnpackedPermissionSchema.array().parse( UnpackedPermissionSchema.array().parse(
@@ -204,7 +195,6 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
}); });
return { return {
...additionalPrivilege, ...additionalPrivilege,
permissions: unpackPermissions(additionalPrivilege.permissions) permissions: unpackPermissions(additionalPrivilege.permissions)
}; };
}; };
@@ -325,7 +315,6 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
}); });
return identityPrivileges.map((el) => ({ return identityPrivileges.map((el) => ({
...el, ...el,
permissions: unpackPermissions(el.permissions) permissions: unpackPermissions(el.permissions)
})); }));
}; };
@@ -1,9 +1,8 @@
import { AbilityBuilder, createMongoAbility, ForcedSubject, MongoAbility } from "@casl/ability"; import { AbilityBuilder, createMongoAbility, ForcedSubject, MongoAbility } from "@casl/ability";
import { z } from "zod"; import { z } from "zod";
import { TableName } from "@app/db/schemas";
import { conditionsMatcher } from "@app/lib/casl"; import { conditionsMatcher } from "@app/lib/casl";
import { BadRequestError } from "@app/lib/errors"; import { UnpackedPermissionSchema } from "@app/server/routes/santizedSchemas/permission";
import { PermissionConditionOperators, PermissionConditionSchema } from "./permission-types"; import { PermissionConditionOperators, PermissionConditionSchema } from "./permission-types";
@@ -752,17 +751,35 @@ export const isAtLeastAsPrivilegedWorkspace = (
}; };
/* eslint-enable */ /* eslint-enable */
export const SecretV2SubjectFieldMapper = (arg: string) => { export const backfillPermissionV1SchemaToV2Schema = (data: z.infer<typeof ProjectPermissionV1Schema>[]) => {
switch (arg) { const formattedData = UnpackedPermissionSchema.array().parse(data);
case "environment": const secretSubjects = formattedData.filter((el) => el.subject === ProjectPermissionSub.Secrets);
return null;
case "secretPath": // this means the folder permission as readonly is set
return null; const hasReadOnlyFolder = formattedData.filter((el) => el.subject === ProjectPermissionSub.SecretFolders);
case "secretName": const secretImportPolicies = secretSubjects.map(({ subject, ...el }) => ({
return `${TableName.SecretV2}.key`; ...el,
case "secretTags": subject: ProjectPermissionSub.SecretImports as const
return `${TableName.SecretTag}.slug`; }));
default:
throw new BadRequestError({ message: `Invalid dynamic knex operator field: ${arg}` }); const secretFolderPolicies = secretSubjects.map(({ subject, ...el }) => ({
} ...el,
subject: ProjectPermissionSub.DynamicSecrets
}));
const dynamicSecretPolicies = secretSubjects.map(({ subject, ...el }) => ({
...el,
action: el.action.includes(ProjectPermissionActions.Edit)
? [...el.action, ProjectPermissionDynamicSecretActions.Lease]
: el.action,
subject: ProjectPermissionSub.DynamicSecrets
}));
return formattedData.concat(
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore-error this is valid ts
secretImportPolicies,
dynamicSecretPolicies,
hasReadOnlyFolder.length ? [] : secretFolderPolicies
);
}; };
@@ -1,11 +1,13 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError, MongoAbility, RawRuleOf } from "@casl/ability";
import { PackRule, unpackRules } from "@casl/ability/extra";
import ms from "ms"; import ms from "ms";
import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { UnpackedPermissionSchema } from "@app/server/routes/santizedSchemas/permission";
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal"; import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
import { TPermissionServiceFactory } from "../permission/permission-service"; import { TPermissionServiceFactory } from "../permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission"; import { ProjectPermissionActions, ProjectPermissionSet, ProjectPermissionSub } from "../permission/project-permission";
import { TProjectUserAdditionalPrivilegeDALFactory } from "./project-user-additional-privilege-dal"; import { TProjectUserAdditionalPrivilegeDALFactory } from "./project-user-additional-privilege-dal";
import { import {
ProjectUserAdditionalPrivilegeTemporaryMode, ProjectUserAdditionalPrivilegeTemporaryMode,
@@ -26,6 +28,11 @@ export type TProjectUserAdditionalPrivilegeServiceFactory = ReturnType<
typeof projectUserAdditionalPrivilegeServiceFactory typeof projectUserAdditionalPrivilegeServiceFactory
>; >;
const unpackPermissions = (permissions: unknown) =>
UnpackedPermissionSchema.array().parse(
unpackRules((permissions || []) as PackRule<RawRuleOf<MongoAbility<ProjectPermissionSet>>>[])
);
export const projectUserAdditionalPrivilegeServiceFactory = ({ export const projectUserAdditionalPrivilegeServiceFactory = ({
projectUserAdditionalPrivilegeDAL, projectUserAdditionalPrivilegeDAL,
projectMembershipDAL, projectMembershipDAL,
@@ -82,7 +89,10 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
temporaryAccessStartTime: new Date(dto.temporaryAccessStartTime), temporaryAccessStartTime: new Date(dto.temporaryAccessStartTime),
temporaryAccessEndTime: new Date(new Date(dto.temporaryAccessStartTime).getTime() + relativeTempAllocatedTimeInMs) temporaryAccessEndTime: new Date(new Date(dto.temporaryAccessStartTime).getTime() + relativeTempAllocatedTimeInMs)
}); });
return additionalPrivilege; return {
...additionalPrivilege,
permissions: unpackPermissions(additionalPrivilege.permissions)
};
}; };
const updateById = async ({ const updateById = async ({
@@ -131,7 +141,11 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
temporaryAccessStartTime: new Date(temporaryAccessStartTime || ""), temporaryAccessStartTime: new Date(temporaryAccessStartTime || ""),
temporaryAccessEndTime: new Date(new Date(temporaryAccessStartTime || "").getTime() + ms(temporaryRange || "")) temporaryAccessEndTime: new Date(new Date(temporaryAccessStartTime || "").getTime() + ms(temporaryRange || ""))
}); });
return additionalPrivilege;
return {
...additionalPrivilege,
permissions: unpackPermissions(additionalPrivilege.permissions)
};
} }
const additionalPrivilege = await projectUserAdditionalPrivilegeDAL.updateById(userPrivilege.id, { const additionalPrivilege = await projectUserAdditionalPrivilegeDAL.updateById(userPrivilege.id, {
@@ -142,7 +156,10 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
temporaryRange: null, temporaryRange: null,
temporaryMode: null temporaryMode: null
}); });
return additionalPrivilege; return {
...additionalPrivilege,
permissions: unpackPermissions(additionalPrivilege.permissions)
};
}; };
const deleteById = async ({ actorId, actor, actorOrgId, actorAuthMethod, privilegeId }: TDeleteUserPrivilegeDTO) => { const deleteById = async ({ actorId, actor, actorOrgId, actorAuthMethod, privilegeId }: TDeleteUserPrivilegeDTO) => {
@@ -165,7 +182,10 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member);
const deletedPrivilege = await projectUserAdditionalPrivilegeDAL.deleteById(userPrivilege.id); const deletedPrivilege = await projectUserAdditionalPrivilegeDAL.deleteById(userPrivilege.id);
return deletedPrivilege; return {
...deletedPrivilege,
permissions: unpackPermissions(deletedPrivilege.permissions)
};
}; };
const getPrivilegeDetailsById = async ({ const getPrivilegeDetailsById = async ({
@@ -193,7 +213,10 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
); );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Member); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Member);
return userPrivilege; return {
...userPrivilege,
permissions: unpackPermissions(userPrivilege.permissions)
};
}; };
const listPrivileges = async ({ const listPrivileges = async ({
@@ -219,7 +242,10 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
userId: projectMembership.userId, userId: projectMembership.userId,
projectId: projectMembership.projectId projectId: projectMembership.projectId
}); });
return userPrivileges; return userPrivileges.map((el) => ({
...el,
permissions: unpackPermissions(el.permissions)
}));
}; };
return { return {
@@ -9,9 +9,10 @@ import {
SecretApprovalPoliciesSchema, SecretApprovalPoliciesSchema,
UsersSchema UsersSchema
} from "@app/db/schemas"; } from "@app/db/schemas";
import { UnpackedPermissionSchema } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { UnpackedPermissionSchema } from "./santizedSchemas/permission";
// sometimes the return data must be santizied to avoid leaking important values // sometimes the return data must be santizied to avoid leaking important values
// always prefer pick over omit in zod // always prefer pick over omit in zod
export const integrationAuthPubSchema = IntegrationAuthsSchema.pick({ export const integrationAuthPubSchema = IntegrationAuthsSchema.pick({
@@ -0,0 +1,11 @@
import { z } from "zod";
export const UnpackedPermissionSchema = z.object({
subject: z
.union([z.string().min(1), z.string().array()])
.transform((el) => (typeof el !== "string" ? el[0] : el))
.optional(),
action: z.union([z.string().min(1), z.string().array()]).transform((el) => (typeof el === "string" ? [el] : el)),
conditions: z.unknown().optional(),
inverted: z.boolean().optional()
});
@@ -2,7 +2,6 @@ import { ForbiddenError, MongoAbility, RawRuleOf } from "@casl/ability";
import { PackRule, packRules, unpackRules } from "@casl/ability/extra"; import { PackRule, packRules, unpackRules } from "@casl/ability/extra";
import { ProjectMembershipRole } from "@app/db/schemas"; import { ProjectMembershipRole } from "@app/db/schemas";
import { UnpackedPermissionSchema } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { import {
ProjectPermissionActions, ProjectPermissionActions,
@@ -10,6 +9,7 @@ import {
ProjectPermissionSub ProjectPermissionSub
} from "@app/ee/services/permission/project-permission"; } from "@app/ee/services/permission/project-permission";
import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { UnpackedPermissionSchema } from "@app/server/routes/santizedSchemas/permission";
import { ActorAuthMethod } from "../auth/auth-type"; import { ActorAuthMethod } from "../auth/auth-type";
import { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/identity-project-membership-role-dal"; import { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/identity-project-membership-role-dal";
@@ -1,4 +1,3 @@
import { packRules } from "@casl/ability/extra";
import { useMutation, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request"; import { apiRequest } from "@app/config/request";
@@ -16,10 +15,7 @@ export const useCreateProjectUserAdditionalPrivilege = () => {
return useMutation<{ privilege: TProjectUserPrivilege }, {}, TCreateProjectUserPrivilegeDTO>({ return useMutation<{ privilege: TProjectUserPrivilege }, {}, TCreateProjectUserPrivilegeDTO>({
mutationFn: async (dto) => { mutationFn: async (dto) => {
const { data } = await apiRequest.post("/api/v1/additional-privilege/users/permanent", { const { data } = await apiRequest.post("/api/v1/additional-privilege/users/permanent", dto);
...dto,
permissions: packRules(dto.permissions)
});
return data.privilege; return data.privilege;
}, },
onSuccess: (_, { projectMembershipId }) => { onSuccess: (_, { projectMembershipId }) => {
@@ -35,7 +31,7 @@ export const useUpdateProjectUserAdditionalPrivilege = () => {
mutationFn: async (dto) => { mutationFn: async (dto) => {
const { data } = await apiRequest.patch( const { data } = await apiRequest.patch(
`/api/v1/additional-privilege/users/${dto.privilegeId}`, `/api/v1/additional-privilege/users/${dto.privilegeId}`,
{ ...dto, permissions: dto.permissions ? packRules(dto.permissions) : undefined } dto
); );
return data.privilege; return data.privilege;
}, },
@@ -1,4 +1,3 @@
import { PackRule, unpackRules } from "@casl/ability/extra";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request"; import { apiRequest } from "@app/config/request";
@@ -18,10 +17,7 @@ const fetchProjectUserPrivilegeDetails = async (privilegeId: string) => {
} = await apiRequest.get<{ } = await apiRequest.get<{
privilege: Omit<TProjectUserPrivilege, "permissions"> & { permissions: unknown }; privilege: Omit<TProjectUserPrivilege, "permissions"> & { permissions: unknown };
}>(`/api/v1/additional-privilege/users/${privilegeId}`); }>(`/api/v1/additional-privilege/users/${privilegeId}`);
return { return privilege;
...privilege,
permissions: unpackRules(privilege.permissions as PackRule<TProjectPermission>[])
};
}; };
export const useGetProjectUserPrivilegeDetails = (privilegeId: string) => { export const useGetProjectUserPrivilegeDetails = (privilegeId: string) => {
@@ -44,7 +40,7 @@ export const useListProjectUserPrivileges = (projectMembershipId: string) => {
}>("/api/v1/additional-privilege/users", { params: { projectMembershipId } }); }>("/api/v1/additional-privilege/users", { params: { projectMembershipId } });
return privileges.map((el) => ({ return privileges.map((el) => ({
...el, ...el,
permissions: unpackRules(el.permissions as PackRule<TProjectPermission>[]) permissions: el.permissions as TProjectPermission[]
})); }));
} }
}); });
@@ -4,6 +4,15 @@ export enum ProjectUserAdditionalPrivilegeTemporaryMode {
Relative = "relative" Relative = "relative"
} }
export type TProjectSpecificPrivilegePermission = {
conditions: {
environment: string;
secretPath?: { $glob: string };
};
actions: string[];
subject: string;
};
export type TProjectUserPrivilege = { export type TProjectUserPrivilege = {
projectMembershipId: string; projectMembershipId: string;
slug: string; slug: string;
@@ -12,21 +21,21 @@ export type TProjectUserPrivilege = {
updatedAt: Date; updatedAt: Date;
permissions?: TProjectPermission[]; permissions?: TProjectPermission[];
} & ( } & (
| { | {
isTemporary: true; isTemporary: true;
temporaryMode: string; temporaryMode: string;
temporaryRange: string; temporaryRange: string;
temporaryAccessStartTime: string; temporaryAccessStartTime: string;
temporaryAccessEndTime?: string; temporaryAccessEndTime?: string;
} }
| { | {
isTemporary: false; isTemporary: false;
temporaryMode?: null; temporaryMode?: null;
temporaryRange?: null; temporaryRange?: null;
temporaryAccessStartTime?: null; temporaryAccessStartTime?: null;
temporaryAccessEndTime?: null; temporaryAccessEndTime?: null;
} }
); );
export type TCreateProjectUserPrivilegeDTO = { export type TCreateProjectUserPrivilegeDTO = {
projectMembershipId: string; projectMembershipId: string;
@@ -35,7 +44,7 @@ export type TCreateProjectUserPrivilegeDTO = {
temporaryMode?: ProjectUserAdditionalPrivilegeTemporaryMode; temporaryMode?: ProjectUserAdditionalPrivilegeTemporaryMode;
temporaryRange?: string; temporaryRange?: string;
temporaryAccessStartTime?: string; temporaryAccessStartTime?: string;
permissions: TProjectPermission[]; permissions: TProjectSpecificPrivilegePermission;
}; };
export type TUpdateProjectUserPrivlegeDTO = { export type TUpdateProjectUserPrivlegeDTO = {
@@ -184,20 +184,20 @@ export const SpecificPrivilegeSecretForm = ({
{ action: ProjectPermissionActions.Delete, allowed: data.delete }, { action: ProjectPermissionActions.Delete, allowed: data.delete },
{ action: ProjectPermissionActions.Edit, allowed: data.edit } { action: ProjectPermissionActions.Edit, allowed: data.edit }
]; ];
const conditions: Record<string, any> = { environment: data.environmentSlug }; const conditions: { environment: string; secretPath?: { $glob: string } } = {
environment: data.environmentSlug
};
if (data.secretPath) { if (data.secretPath) {
conditions.secretPath = { $glob: removeTrailingSlash(data.secretPath) }; conditions.secretPath = { $glob: removeTrailingSlash(data.secretPath) };
} }
await updateUserPrivilege.mutateAsync({ await updateUserPrivilege.mutateAsync({
privilegeId: privilege.id, privilegeId: privilege.id,
...data.temporaryAccess, ...data.temporaryAccess,
permissions: actions permissions: {
.filter(({ allowed }) => allowed) subject: ProjectPermissionSub.Secrets,
.map(({ action }) => ({ conditions,
action, actions: actions.filter((i) => i.allowed).map((i) => i.action)
subject: [ProjectPermissionSub.Secrets], },
conditions
})),
projectMembershipId: privilege.projectMembershipId projectMembershipId: privilege.projectMembershipId
}); });
createNotification({ createNotification({
@@ -642,15 +642,13 @@ export const SpecificPrivilegeSection = ({ membershipId }: Props) => {
if (createUserPrivilege.isLoading) return; if (createUserPrivilege.isLoading) return;
try { try {
await createUserPrivilege.mutateAsync({ await createUserPrivilege.mutateAsync({
permissions: [ permissions: {
{ actions: [ProjectPermissionActions.Read],
action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.Secrets,
subject: [ProjectPermissionSub.Secrets], conditions: {
conditions: { environment: currentWorkspace?.environments?.[0].slug || ""
environment: currentWorkspace?.environments?.[0].slug
}
} }
], },
projectMembershipId: membershipId projectMembershipId: membershipId
}); });
createNotification({ createNotification({