Compare commits

...

4 Commits

9 changed files with 393 additions and 71 deletions

View File

@ -2,6 +2,7 @@ import { nanoid } from "nanoid";
import { z } from "zod";
import { EnforcementLevel } from "@app/lib/types";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { sapPubSchema } from "@app/server/routes/sanitizedSchemas";
import { AuthMode } from "@app/services/auth/auth-type";
@ -10,6 +11,9 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
server.route({
url: "/",
method: "POST",
config: {
rateLimit: writeLimit
},
schema: {
body: z
.object({
@ -17,11 +21,16 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
name: z.string().optional(),
secretPath: z.string().trim().default("/"),
environment: z.string(),
approvers: z.string().array().min(1),
approvers: z.string().array().min(1).optional(),
approverUsernames: z.string().array().min(1).optional(),
approvals: z.number().min(1).default(1),
enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard)
})
.refine((data) => data.approvals <= data.approvers.length, {
.refine((data) => data.approvers || data.approverUsernames, {
path: ["approvers", "approverUsernames"],
message: "Either approvers or approverUsernames must be defined"
})
.refine((data) => data.approvals <= Number(data.approvers?.length ?? data.approverUsernames?.length), {
path: ["approvals"],
message: "The number of approvals should be lower than the number of approvers."
}),
@ -31,7 +40,7 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const approval = await server.services.accessApprovalPolicy.createAccessApprovalPolicy({
actor: req.permission.type,
@ -50,6 +59,9 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
server.route({
url: "/",
method: "GET",
config: {
rateLimit: readLimit
},
schema: {
querystring: z.object({
projectSlug: z.string().trim()
@ -86,6 +98,9 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
server.route({
url: "/count",
method: "GET",
config: {
rateLimit: readLimit
},
schema: {
querystring: z.object({
projectSlug: z.string(),
@ -115,6 +130,9 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
server.route({
url: "/:policyId",
method: "PATCH",
config: {
rateLimit: writeLimit
},
schema: {
params: z.object({
policyId: z.string()
@ -127,11 +145,16 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
.trim()
.optional()
.transform((val) => (val === "" ? "/" : val)),
approvers: z.string().array().min(1),
approvers: z.string().array().min(1).optional(),
approverUsernames: z.string().array().min(1).optional(),
approvals: z.number().min(1).default(1),
enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard)
})
.refine((data) => data.approvals <= data.approvers.length, {
.refine((data) => data.approvers || data.approverUsernames, {
path: ["approvers", "approverUsernames"],
message: "Either approvers or approverUsernames must be defined"
})
.refine((data) => data.approvals <= Number(data.approvers?.length ?? data.approverUsernames?.length), {
path: ["approvals"],
message: "The number of approvals should be lower than the number of approvers."
}),
@ -141,7 +164,7 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
await server.services.accessApprovalPolicy.updateAccessApprovalPolicy({
policyId: req.params.policyId,
@ -167,7 +190,7 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const approval = await server.services.accessApprovalPolicy.deleteAccessApprovalPolicy({
actor: req.permission.type,
@ -176,6 +199,43 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
actorOrgId: req.permission.orgId,
policyId: req.params.policyId
});
return { approval };
}
});
server.route({
url: "/:policyId",
method: "GET",
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
policyId: z.string()
}),
response: {
200: z.object({
approval: sapPubSchema.extend({
userApprovers: z
.object({
userId: z.string(),
username: z.string()
})
.array()
})
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const approval = await server.services.accessApprovalPolicy.getAccessApprovalPolicyById({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.params
});
return { approval };
}
});

View File

@ -27,11 +27,16 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
.nullable()
.default("/")
.transform((val) => (val ? removeTrailingSlash(val) : val)),
approvers: z.string().array().min(1),
approvers: z.string().array().min(1).optional(),
approverUsernames: z.string().array().min(1).optional(),
approvals: z.number().min(1).default(1),
enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard)
})
.refine((data) => data.approvals <= data.approvers.length, {
.refine((data) => data.approvers || data.approverUsernames, {
path: ["approvers", "approverUsernames"],
message: "Either approvers or approverUsernames must be defined"
})
.refine((data) => data.approvals <= Number(data.approvers?.length ?? data.approverUsernames?.length), {
path: ["approvals"],
message: "The number of approvals should be lower than the number of approvers."
}),
@ -41,7 +46,7 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const approval = await server.services.secretApprovalPolicy.createSecretApprovalPolicy({
actor: req.permission.type,
@ -53,6 +58,7 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
name: req.body.name ?? `${req.body.environment}-${nanoid(3)}`,
enforcementLevel: req.body.enforcementLevel
});
return { approval };
}
});
@ -70,7 +76,8 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
body: z
.object({
name: z.string().optional(),
approvers: z.string().array().min(1),
approvers: z.string().array().min(1).optional(),
approverUsernames: z.string().array().min(1).optional(),
approvals: z.number().min(1).default(1),
secretPath: z
.string()
@ -80,7 +87,11 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
.transform((val) => (val === "" ? "/" : val)),
enforcementLevel: z.nativeEnum(EnforcementLevel).optional()
})
.refine((data) => data.approvals <= data.approvers.length, {
.refine((data) => data.approvers || data.approverUsernames, {
path: ["approvers", "approverUsernames"],
message: "Either approvers or approverUsernames must be defined"
})
.refine((data) => data.approvals <= Number(data.approvers?.length ?? data.approverUsernames?.length), {
path: ["approvals"],
message: "The number of approvals should be lower than the number of approvers."
}),
@ -90,7 +101,7 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const approval = await server.services.secretApprovalPolicy.updateSecretApprovalPolicy({
actor: req.permission.type,
@ -120,7 +131,7 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const approval = await server.services.secretApprovalPolicy.deleteSecretApprovalPolicy({
actor: req.permission.type,
@ -149,7 +160,8 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
.extend({
userApprovers: z
.object({
userId: z.string()
userId: z.string(),
username: z.string()
})
.array()
})
@ -157,7 +169,7 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const approvals = await server.services.secretApprovalPolicy.getSecretApprovalPolicyByProjectId({
actor: req.permission.type,
@ -170,6 +182,42 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
}
});
server.route({
url: "/:sapId",
method: "GET",
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
sapId: z.string()
}),
response: {
200: z.object({
approval: sapPubSchema.extend({
userApprovers: z
.object({
userId: z.string(),
username: z.string()
})
.array()
})
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const approval = await server.services.secretApprovalPolicy.getSecretApprovalPolicyById({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.params
});
return { approval };
}
});
server.route({
url: "/board",
method: "GET",

View File

@ -10,17 +10,32 @@ export type TAccessApprovalPolicyDALFactory = ReturnType<typeof accessApprovalPo
export const accessApprovalPolicyDALFactory = (db: TDbClient) => {
const accessApprovalPolicyOrm = ormify(db, TableName.AccessApprovalPolicy);
const accessApprovalPolicyFindQuery = async (tx: Knex, filter: TFindFilter<TAccessApprovalPolicies>) => {
const accessApprovalPolicyFindQuery = async (
tx: Knex,
filter: TFindFilter<TAccessApprovalPolicies>,
customFilter?: {
policyId?: string;
}
) => {
const result = await tx(TableName.AccessApprovalPolicy)
// eslint-disable-next-line
.where(buildFindFilter(filter))
.where((qb) => {
if (customFilter?.policyId) {
void qb.where(`${TableName.AccessApprovalPolicy}.id`, "=", customFilter.policyId);
}
})
.join(TableName.Environment, `${TableName.AccessApprovalPolicy}.envId`, `${TableName.Environment}.id`)
.leftJoin(
TableName.AccessApprovalPolicyApprover,
`${TableName.AccessApprovalPolicy}.id`,
`${TableName.AccessApprovalPolicyApprover}.policyId`
)
.select(tx.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover))
.leftJoin(TableName.Users, `${TableName.AccessApprovalPolicyApprover}.approverUserId`, `${TableName.Users}.id`)
.select(
tx.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover),
tx.ref("username").withSchema(TableName.Users).as("approverUsername")
)
.select(tx.ref("name").withSchema(TableName.Environment).as("envName"))
.select(tx.ref("slug").withSchema(TableName.Environment).as("envSlug"))
.select(tx.ref("id").withSchema(TableName.Environment).as("envId"))
@ -64,9 +79,15 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient) => {
}
};
const find = async (filter: TFindFilter<TAccessApprovalPolicies & { projectId: string }>, tx?: Knex) => {
const find = async (
filter: TFindFilter<TAccessApprovalPolicies & { projectId: string }>,
customFilter?: {
policyId?: string;
},
tx?: Knex
) => {
try {
const docs = await accessApprovalPolicyFindQuery(tx || db.replicaNode(), filter);
const docs = await accessApprovalPolicyFindQuery(tx || db.replicaNode(), filter, customFilter);
const formattedDocs = sqlNestRelationships({
data: docs,
@ -85,8 +106,9 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient) => {
{
key: "approverUserId",
label: "userApprovers" as const,
mapper: ({ approverUserId }) => ({
userId: approverUserId
mapper: ({ approverUserId, approverUsername }) => ({
userId: approverUserId,
username: approverUsername
})
}
]

View File

@ -2,10 +2,11 @@ import { ForbiddenError } from "@casl/ability";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { BadRequestError } from "@app/lib/errors";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal";
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
import { TUserDALFactory } from "@app/services/user/user-dal";
import { TAccessApprovalPolicyApproverDALFactory } from "./access-approval-policy-approver-dal";
import { TAccessApprovalPolicyDALFactory } from "./access-approval-policy-dal";
@ -13,6 +14,7 @@ import { verifyApprovers } from "./access-approval-policy-fns";
import {
TCreateAccessApprovalPolicy,
TDeleteAccessApprovalPolicy,
TGetAccessApprovalPolicyByIdDTO,
TGetAccessPolicyCountByEnvironmentDTO,
TListAccessApprovalPoliciesDTO,
TUpdateAccessApprovalPolicy
@ -23,6 +25,7 @@ type TSecretApprovalPolicyServiceFactoryDep = {
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
accessApprovalPolicyDAL: TAccessApprovalPolicyDALFactory;
projectEnvDAL: Pick<TProjectEnvDALFactory, "find" | "findOne">;
userDAL: Pick<TUserDALFactory, "find">;
accessApprovalPolicyApproverDAL: TAccessApprovalPolicyApproverDALFactory;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find">;
};
@ -34,6 +37,7 @@ export const accessApprovalPolicyServiceFactory = ({
accessApprovalPolicyApproverDAL,
permissionService,
projectEnvDAL,
userDAL,
projectDAL
}: TSecretApprovalPolicyServiceFactoryDep) => {
const createAccessApprovalPolicy = async ({
@ -45,15 +49,15 @@ export const accessApprovalPolicyServiceFactory = ({
actorAuthMethod,
approvals,
approvers,
approverUsernames,
projectSlug,
environment,
enforcementLevel
}: TCreateAccessApprovalPolicy) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new BadRequestError({ message: "Project not found" });
if (approvals > approvers.length)
throw new BadRequestError({ message: "Approvals cannot be greater than approvers" });
if (!project) {
throw new BadRequestError({ message: "Project not found" });
}
const { permission } = await permissionService.getProjectPermission(
actor,
@ -69,6 +73,25 @@ export const accessApprovalPolicyServiceFactory = ({
const env = await projectEnvDAL.findOne({ slug: environment, projectId: project.id });
if (!env) throw new BadRequestError({ message: "Environment not found" });
let approverIds = approvers;
if (!approverIds) {
const approverUsers = await userDAL.find({
$in: {
username: approverUsernames
}
});
approverIds = approverUsers.map((user) => user.id);
const approverNamesFromDb = approverUsers.map((user) => user.username);
const invalidUsernames = approverUsernames?.filter((username) => !approverNamesFromDb.includes(username));
if (invalidUsernames?.length) {
throw new BadRequestError({
message: `Invalid approver user: ${invalidUsernames.join(", ")}`
});
}
}
await verifyApprovers({
projectId: project.id,
orgId: actorOrgId,
@ -76,7 +99,7 @@ export const accessApprovalPolicyServiceFactory = ({
secretPath,
actorAuthMethod,
permissionService,
userIds: approvers
userIds: approverIds
});
const accessApproval = await accessApprovalPolicyDAL.transaction(async (tx) => {
@ -90,15 +113,20 @@ export const accessApprovalPolicyServiceFactory = ({
},
tx
);
await accessApprovalPolicyApproverDAL.insertMany(
approvers.map((userId) => ({
approverUserId: userId,
policyId: doc.id
})),
tx
);
if (approverIds) {
await accessApprovalPolicyApproverDAL.insertMany(
approverIds.map((userId) => ({
approverUserId: userId,
policyId: doc.id
})),
tx
);
}
return doc;
});
return { ...accessApproval, environment: env, projectId: project.id };
};
@ -129,6 +157,7 @@ export const accessApprovalPolicyServiceFactory = ({
const updateAccessApprovalPolicy = async ({
policyId,
approvers,
approverUsernames,
secretPath,
name,
actorId,
@ -161,28 +190,53 @@ export const accessApprovalPolicyServiceFactory = ({
},
tx
);
if (approvers) {
await verifyApprovers({
projectId: accessApprovalPolicy.projectId,
orgId: actorOrgId,
envSlug: accessApprovalPolicy.environment.slug,
secretPath: doc.secretPath!,
actorAuthMethod,
permissionService,
userIds: approvers
});
if (approvers || approverUsernames) {
let approverIds = approvers;
if (!approverIds) {
const approverUsers = await userDAL.find(
{
$in: {
username: approverUsernames
}
},
{ tx }
);
await accessApprovalPolicyApproverDAL.delete({ policyId: doc.id }, tx);
await accessApprovalPolicyApproverDAL.insertMany(
approvers.map((userId) => ({
approverUserId: userId,
policyId: doc.id
})),
tx
);
approverIds = approverUsers.map((user) => user.id);
const approverNamesFromDb = approverUsers.map((user) => user.username);
const invalidUsernames = approverUsernames?.filter((username) => !approverNamesFromDb.includes(username));
if (invalidUsernames?.length) {
throw new BadRequestError({
message: `Invalid approver user: ${invalidUsernames.join(", ")}`
});
}
}
if (approverIds) {
await verifyApprovers({
projectId: accessApprovalPolicy.projectId,
orgId: actorOrgId,
envSlug: accessApprovalPolicy.environment.slug,
secretPath: doc.secretPath!,
actorAuthMethod,
permissionService,
userIds: approverIds
});
await accessApprovalPolicyApproverDAL.delete({ policyId: doc.id }, tx);
await accessApprovalPolicyApproverDAL.insertMany(
approverIds.map((userId) => ({
approverUserId: userId,
policyId: doc.id
})),
tx
);
}
}
return doc;
});
return {
...updatedPolicy,
environment: accessApprovalPolicy.environment,
@ -246,11 +300,40 @@ export const accessApprovalPolicyServiceFactory = ({
return { count: policies.length };
};
const getAccessApprovalPolicyById = async ({
actorId,
actor,
actorOrgId,
actorAuthMethod,
policyId
}: TGetAccessApprovalPolicyByIdDTO) => {
const [policy] = await accessApprovalPolicyDAL.find({}, { policyId });
if (!policy) {
throw new NotFoundError({
message: "Cannot find access approval policy"
});
}
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
policy.projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
return policy;
};
return {
getAccessPolicyCountByEnvSlug,
createAccessApprovalPolicy,
deleteAccessApprovalPolicy,
updateAccessApprovalPolicy,
getAccessApprovalPolicyByProjectSlug
getAccessApprovalPolicyByProjectSlug,
getAccessApprovalPolicyById
};
};

View File

@ -17,7 +17,8 @@ export type TCreateAccessApprovalPolicy = {
approvals: number;
secretPath: string;
environment: string;
approvers: string[];
approvers?: string[];
approverUsernames?: string[];
projectSlug: string;
name: string;
enforcementLevel: EnforcementLevel;
@ -27,6 +28,7 @@ export type TUpdateAccessApprovalPolicy = {
policyId: string;
approvals?: number;
approvers?: string[];
approverUsernames?: string[];
secretPath?: string;
name?: string;
enforcementLevel?: EnforcementLevel;
@ -44,3 +46,7 @@ export type TGetAccessPolicyCountByEnvironmentDTO = {
export type TListAccessApprovalPoliciesDTO = {
projectSlug: string;
} & Omit<TProjectPermission, "projectId">;
export type TGetAccessApprovalPolicyByIdDTO = {
policyId: string;
} & Omit<TProjectPermission, "projectId">;

View File

@ -10,10 +10,21 @@ export type TSecretApprovalPolicyDALFactory = ReturnType<typeof secretApprovalPo
export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
const secretApprovalPolicyOrm = ormify(db, TableName.SecretApprovalPolicy);
const secretApprovalPolicyFindQuery = (tx: Knex, filter: TFindFilter<TSecretApprovalPolicies>) =>
const secretApprovalPolicyFindQuery = (
tx: Knex,
findFilter: TFindFilter<TSecretApprovalPolicies>,
customFilter?: {
sapId?: string;
}
) =>
tx(TableName.SecretApprovalPolicy)
// eslint-disable-next-line
.where(buildFindFilter(filter))
.where(buildFindFilter(findFilter))
.where((qb) => {
if (customFilter?.sapId) {
void qb.where(`${TableName.SecretApprovalPolicy}.id`, "=", customFilter.sapId);
}
})
.join(TableName.Environment, `${TableName.SecretApprovalPolicy}.envId`, `${TableName.Environment}.id`)
.leftJoin(
TableName.SecretApprovalPolicyApprover,
@ -26,6 +37,7 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
.select(
tx.ref("approverUserId").withSchema(TableName.SecretApprovalPolicyApprover),
tx.ref("email").withSchema(TableName.Users).as("approverEmail"),
tx.ref("username").withSchema(TableName.Users).as("approverUsername"),
tx.ref("firstName").withSchema(TableName.Users).as("approverFirstName"),
tx.ref("lastName").withSchema(TableName.Users).as("approverLastName")
)
@ -71,9 +83,15 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
}
};
const find = async (filter: TFindFilter<TSecretApprovalPolicies & { projectId: string }>, tx?: Knex) => {
const find = async (
filter: TFindFilter<TSecretApprovalPolicies & { projectId: string }>,
customFilter?: {
sapId?: string;
},
tx?: Knex
) => {
try {
const docs = await secretApprovalPolicyFindQuery(tx || db.replicaNode(), filter);
const docs = await secretApprovalPolicyFindQuery(tx || db.replicaNode(), filter, customFilter);
const formatedDoc = sqlNestRelationships({
data: docs,
key: "id",
@ -86,8 +104,9 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
{
key: "approverUserId",
label: "userApprovers" as const,
mapper: ({ approverUserId }) => ({
userId: approverUserId
mapper: ({ approverUserId, approverUsername }) => ({
userId: approverUserId,
username: approverUsername
})
}
]

View File

@ -3,10 +3,11 @@ import picomatch from "picomatch";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { BadRequestError } from "@app/lib/errors";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { removeTrailingSlash } from "@app/lib/fn";
import { containsGlobPatterns } from "@app/lib/picomatch";
import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal";
import { TUserDALFactory } from "@app/services/user/user-dal";
import { TLicenseServiceFactory } from "../license/license-service";
import { TSecretApprovalPolicyApproverDALFactory } from "./secret-approval-policy-approver-dal";
@ -15,6 +16,7 @@ import {
TCreateSapDTO,
TDeleteSapDTO,
TGetBoardSapDTO,
TGetSapByIdDTO,
TListSapDTO,
TUpdateSapDTO
} from "./secret-approval-policy-types";
@ -28,6 +30,7 @@ type TSecretApprovalPolicyServiceFactoryDep = {
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
secretApprovalPolicyDAL: TSecretApprovalPolicyDALFactory;
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
userDAL: Pick<TUserDALFactory, "find">;
secretApprovalPolicyApproverDAL: TSecretApprovalPolicyApproverDALFactory;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
};
@ -37,6 +40,7 @@ export type TSecretApprovalPolicyServiceFactory = ReturnType<typeof secretApprov
export const secretApprovalPolicyServiceFactory = ({
secretApprovalPolicyDAL,
permissionService,
userDAL,
secretApprovalPolicyApproverDAL,
projectEnvDAL,
licenseService
@ -49,14 +53,12 @@ export const secretApprovalPolicyServiceFactory = ({
actorAuthMethod,
approvals,
approvers,
approverUsernames,
projectId,
secretPath,
environment,
enforcementLevel
}: TCreateSapDTO) => {
if (approvals > approvers.length)
throw new BadRequestError({ message: "Approvals cannot be greater than approvers" });
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
@ -81,6 +83,28 @@ export const secretApprovalPolicyServiceFactory = ({
if (!env) throw new BadRequestError({ message: "Environment not found" });
const secretApproval = await secretApprovalPolicyDAL.transaction(async (tx) => {
let approverIds = approvers;
if (!approverIds) {
const approverUsers = await userDAL.find(
{
$in: {
username: approverUsernames
}
},
{ tx }
);
approverIds = approverUsers.map((user) => user.id);
const approverNamesFromDb = approverUsers.map((user) => user.username);
const invalidUsernames = approverUsernames?.filter((username) => !approverNamesFromDb.includes(username));
if (invalidUsernames?.length) {
throw new BadRequestError({
message: `Invalid approver user: ${invalidUsernames.join(", ")}`
});
}
}
const doc = await secretApprovalPolicyDAL.create(
{
envId: env.id,
@ -91,8 +115,9 @@ export const secretApprovalPolicyServiceFactory = ({
},
tx
);
await secretApprovalPolicyApproverDAL.insertMany(
approvers.map((approverUserId) => ({
approverIds.map((approverUserId) => ({
approverUserId,
policyId: doc.id
})),
@ -105,6 +130,7 @@ export const secretApprovalPolicyServiceFactory = ({
const updateSecretApprovalPolicy = async ({
approvers,
approverUsernames,
secretPath,
name,
actorId,
@ -146,16 +172,39 @@ export const secretApprovalPolicyServiceFactory = ({
},
tx
);
if (approvers) {
if (approvers || approverUsernames) {
let approverIds = approvers;
if (!approverIds) {
const approverUsers = await userDAL.find(
{
$in: {
username: approverUsernames
}
},
{ tx }
);
approverIds = approverUsers.map((user) => user.id);
const approverNamesFromDb = approverUsers.map((user) => user.username);
const invalidUsernames = approverUsernames?.filter((username) => !approverNamesFromDb.includes(username));
if (invalidUsernames?.length) {
throw new BadRequestError({
message: `Invalid approver user: ${invalidUsernames.join(", ")}`
});
}
}
await secretApprovalPolicyApproverDAL.delete({ policyId: doc.id }, tx);
await secretApprovalPolicyApproverDAL.insertMany(
approvers.map((approverUserId) => ({
approverIds.map((approverUserId) => ({
approverUserId,
policyId: doc.id
})),
tx
);
}
return doc;
});
return {
@ -219,6 +268,34 @@ export const secretApprovalPolicyServiceFactory = ({
return sapPolicies;
};
const getSecretApprovalPolicyById = async ({
actorId,
actor,
actorOrgId,
actorAuthMethod,
sapId
}: TGetSapByIdDTO) => {
const [sapPolicy] = await secretApprovalPolicyDAL.find({}, { sapId });
if (!sapPolicy) {
throw new NotFoundError({
message: "Cannot find secret approval policy"
});
}
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
sapPolicy.projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
return sapPolicy;
};
const getSecretApprovalPolicy = async (projectId: string, environment: string, path: string) => {
const secretPath = removeTrailingSlash(path);
const env = await projectEnvDAL.findOne({ slug: environment, projectId });
@ -262,6 +339,7 @@ export const secretApprovalPolicyServiceFactory = ({
return {
createSecretApprovalPolicy,
getSecretApprovalPolicyById,
updateSecretApprovalPolicy,
deleteSecretApprovalPolicy,
getSecretApprovalPolicy,

View File

@ -4,7 +4,8 @@ export type TCreateSapDTO = {
approvals: number;
secretPath?: string | null;
environment: string;
approvers: string[];
approvers?: string[];
approverUsernames?: string[];
projectId: string;
name: string;
enforcementLevel: EnforcementLevel;
@ -14,7 +15,8 @@ export type TUpdateSapDTO = {
secretPolicyId: string;
approvals?: number;
secretPath?: string | null;
approvers: string[];
approvers?: string[];
approverUsernames?: string[];
name?: string;
enforcementLevel?: EnforcementLevel;
} & Omit<TProjectPermission, "projectId">;
@ -25,6 +27,8 @@ export type TDeleteSapDTO = {
export type TListSapDTO = TProjectPermission;
export type TGetSapByIdDTO = Omit<TProjectPermission, "projectId"> & { sapId: string };
export type TGetBoardSapDTO = {
projectId: string;
environment: string;

View File

@ -380,6 +380,7 @@ export const registerRoutes = async (
secretApprovalPolicyApproverDAL: sapApproverDAL,
permissionService,
secretApprovalPolicyDAL,
userDAL,
licenseService
});
const tokenService = tokenServiceFactory({ tokenDAL: authTokenDAL, userDAL, orgMembershipDAL });
@ -926,7 +927,8 @@ export const registerRoutes = async (
permissionService,
projectEnvDAL,
projectMembershipDAL,
projectDAL
projectDAL,
userDAL
});
const accessApprovalRequestService = accessApprovalRequestServiceFactory({