diff --git a/server/auth/actions.ts b/server/auth/actions.ts index b3e6c5f75..5c512181a 100644 --- a/server/auth/actions.ts +++ b/server/auth/actions.ts @@ -144,7 +144,8 @@ export enum ActionsEnum { setResourcePolicyUsers = "setResourcePolicyUsers", setResourcePolicyPassword = "setResourcePolicyPassword", setResourcePolicyPincode = "setResourcePolicyPincode", - setResourcePolicyHeaderAuth = "setResourcePolicyHeaderAuth" + setResourcePolicyHeaderAuth = "setResourcePolicyHeaderAuth", + setResourcePolicyWhitelist = "setResourcePolicyWhitelist" } export async function checkUserActionPermission( diff --git a/server/routers/external.ts b/server/routers/external.ts index 379ff794c..faee76486 100644 --- a/server/routers/external.ts +++ b/server/routers/external.ts @@ -761,6 +761,15 @@ authenticated.put( policy.setResourcePolicyHeaderAuth ); +authenticated.put( + "/resource-policy/:resourcePolicyId/whitelist", + verifyResourcePolicyAccess, + verifyLimits, + verifyUserHasAction(ActionsEnum.setResourcePolicyWhitelist), + logActionAudit(ActionsEnum.setResourcePolicyWhitelist), + policy.setResourcePolicyWhitelist +); + authenticated.post( `/resource/:resourceId/password`, verifyResourceAccess, diff --git a/server/routers/integration.ts b/server/routers/integration.ts index 52c839b18..89ec2c2d7 100644 --- a/server/routers/integration.ts +++ b/server/routers/integration.ts @@ -659,6 +659,15 @@ authenticated.put( policy.setResourcePolicyHeaderAuth ); +authenticated.put( + "/resource-policy/:resourcePolicyId/whitelist", + verifyApiKeyResourcePolicyAccess, + verifyLimits, + verifyApiKeyHasAction(ActionsEnum.setResourcePolicyWhitelist), + logActionAudit(ActionsEnum.setResourcePolicyWhitelist), + policy.setResourcePolicyWhitelist +); + authenticated.post( "/resource/:resourceId/roles/add", verifyApiKeyResourceAccess, diff --git a/server/routers/policy/index.ts b/server/routers/policy/index.ts index 9d191af15..7719ffdfe 100644 --- a/server/routers/policy/index.ts +++ b/server/routers/policy/index.ts @@ -4,3 +4,4 @@ export * from "./setResourcePolicyAccessControl"; export * from "./setResourcePolicyPassword"; export * from "./setResourcePolicyPincode"; export * from "./setResourcePolicyHeaderAuth"; +export * from "./setResourcePolicyWhitelist"; diff --git a/server/routers/policy/setResourcePolicyWhitelist.ts b/server/routers/policy/setResourcePolicyWhitelist.ts new file mode 100644 index 000000000..63fabeaa0 --- /dev/null +++ b/server/routers/policy/setResourcePolicyWhitelist.ts @@ -0,0 +1,132 @@ +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db, resourcePolicies, resourcePolicyWhiteList } from "@server/db"; +import response from "@server/lib/response"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import logger from "@server/logger"; +import { fromError } from "zod-validation-error"; +import { and, eq } from "drizzle-orm"; +import { OpenAPITags, registry } from "@server/openApi"; + +const setResourcePolicyWhitelistBodySchema = z.strictObject({ + emailWhitelistEnabled: z.boolean(), + emails: z + .array( + z.email().or( + z.string().regex(/^\*@[\w.-]+\.[a-zA-Z]{2,}$/, { + error: "Invalid email address. Wildcard (*) must be the entire local part." + }) + ) + ) + .max(50) + .transform((v) => v.map((e) => e.toLowerCase())) +}); + +const setResourcePolicyWhitelistParamsSchema = z.strictObject({ + resourcePolicyId: z.string().transform(Number).pipe(z.int().positive()) +}); + +registry.registerPath({ + method: "put", + path: "/resource-policy/{resourcePolicyId}/whitelist", + description: + "Set email whitelist for a resource policy. This will replace all existing emails.", + tags: [OpenAPITags.Resource], + request: { + params: setResourcePolicyWhitelistParamsSchema, + body: { + content: { + "application/json": { + schema: setResourcePolicyWhitelistBodySchema + } + } + } + }, + responses: {} +}); + +export async function setResourcePolicyWhitelist( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedBody = setResourcePolicyWhitelistBodySchema.safeParse( + req.body + ); + if (!parsedBody.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedBody.error).toString() + ) + ); + } + + const parsedParams = setResourcePolicyWhitelistParamsSchema.safeParse( + req.params + ); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const { resourcePolicyId } = parsedParams.data; + const { emailWhitelistEnabled, emails } = parsedBody.data; + + const [policy] = await db + .select() + .from(resourcePolicies) + .where(eq(resourcePolicies.resourcePolicyId, resourcePolicyId)); + + if (!policy) { + return next( + createHttpError(HttpCode.NOT_FOUND, "Resource policy not found") + ); + } + + await db.transaction(async (trx) => { + await trx + .update(resourcePolicies) + .set({ emailWhitelistEnabled }) + .where(eq(resourcePolicies.resourcePolicyId, resourcePolicyId)); + + // delete all whitelist emails + await trx + .delete(resourcePolicyWhiteList) + .where( + eq( + resourcePolicyWhiteList.resourcePolicyId, + resourcePolicyId + ) + ); + + if (emailWhitelistEnabled && emails.length > 0) { + await trx.insert(resourcePolicyWhiteList).values( + emails.map((email) => ({ + email, + resourcePolicyId + })) + ); + } + }); + + return response(res, { + data: {}, + success: true, + error: false, + message: "Whitelist set for resource policy successfully", + status: HttpCode.OK + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +}