diff --git a/server/auth/actions.ts b/server/auth/actions.ts index a367cc1ea..b3e6c5f75 100644 --- a/server/auth/actions.ts +++ b/server/auth/actions.ts @@ -141,7 +141,10 @@ export enum ActionsEnum { listResourcePolicyRoles = "listResourcePolicyRoles", setResourcePolicyRoles = "setResourcePolicyRoles", listResourcePolicyUsers = "listResourcePolicyUsers", - setResourcePolicyUsers = "setResourcePolicyUsers" + setResourcePolicyUsers = "setResourcePolicyUsers", + setResourcePolicyPassword = "setResourcePolicyPassword", + setResourcePolicyPincode = "setResourcePolicyPincode", + setResourcePolicyHeaderAuth = "setResourcePolicyHeaderAuth" } export async function checkUserActionPermission( diff --git a/server/db/pg/schema/schema.ts b/server/db/pg/schema/schema.ts index ef7e79166..34d8ecda6 100644 --- a/server/db/pg/schema/schema.ts +++ b/server/db/pg/schema/schema.ts @@ -516,11 +516,6 @@ export const resourceHeaderAuthExtendedCompatibility = pgTable( resourceId: integer("resourceId") .notNull() .references(() => resources.resourceId, { onDelete: "cascade" }), - resourcePolicyId: integer("resourcePolicyId") - .notNull() - .references(() => resourcePolicies.resourcePolicyId, { - onDelete: "cascade" - }), extendedCompatibilityIsActivated: boolean( "extendedCompatibilityIsActivated" ) @@ -571,9 +566,6 @@ export const resourcePolicyPassword = pgTable("resourcePolicyPassword", { export const resourcePolicyHeaderAuth = pgTable("resourcePolicyHeaderAuth", { headerAuthId: serial("headerAuthId").primaryKey(), - resourceId: integer("resourceId") - .notNull() - .references(() => resources.resourceId, { onDelete: "cascade" }), headerAuthHash: varchar("headerAuthHash").notNull(), resourcePolicyId: integer("resourcePolicyId") .notNull() diff --git a/server/routers/external.ts b/server/routers/external.ts index bb4ee7d31..17c4a2558 100644 --- a/server/routers/external.ts +++ b/server/routers/external.ts @@ -734,6 +734,33 @@ authenticated.post( resource.setResourcePolicyUsers ); +authenticated.post( + "/resource-policy/:resourcePolicyId/password", + verifyResourcePolicyAccess, + verifyLimits, + verifyUserHasAction(ActionsEnum.setResourcePolicyPassword), + logActionAudit(ActionsEnum.setResourcePolicyPassword), + policy.setResourcePolicyPassword +); + +authenticated.post( + "/resource-policy/:resourcePolicyId/pincode", + verifyResourcePolicyAccess, + verifyLimits, + verifyUserHasAction(ActionsEnum.setResourcePolicyPincode), + logActionAudit(ActionsEnum.setResourcePolicyPincode), + policy.setResourcePolicyPincode +); + +authenticated.post( + "/resource-policy/:resourcePolicyId/header-auth", + verifyResourcePolicyAccess, + verifyLimits, + verifyUserHasAction(ActionsEnum.setResourcePolicyHeaderAuth), + logActionAudit(ActionsEnum.setResourcePolicyHeaderAuth), + policy.setResourcePolicyHeaderAuth +); + authenticated.post( `/resource/:resourceId/password`, verifyResourceAccess, diff --git a/server/routers/integration.ts b/server/routers/integration.ts index a68bcb555..223a8a24f 100644 --- a/server/routers/integration.ts +++ b/server/routers/integration.ts @@ -632,6 +632,33 @@ authenticated.put( policy.setResourcePolicyAccessControl ); +authenticated.post( + "/resource-policy/:resourcePolicyId/password", + verifyApiKeyResourcePolicyAccess, + verifyLimits, + verifyApiKeyHasAction(ActionsEnum.setResourcePolicyPassword), + logActionAudit(ActionsEnum.setResourcePolicyPassword), + policy.setResourcePolicyPassword +); + +authenticated.post( + "/resource-policy/:resourcePolicyId/pincode", + verifyApiKeyResourcePolicyAccess, + verifyLimits, + verifyApiKeyHasAction(ActionsEnum.setResourcePolicyPincode), + logActionAudit(ActionsEnum.setResourcePolicyPincode), + policy.setResourcePolicyPincode +); + +authenticated.post( + "/resource-policy/:resourcePolicyId/header-auth", + verifyApiKeyResourcePolicyAccess, + verifyLimits, + verifyApiKeyHasAction(ActionsEnum.setResourcePolicyHeaderAuth), + logActionAudit(ActionsEnum.setResourcePolicyHeaderAuth), + policy.setResourcePolicyHeaderAuth +); + authenticated.post( "/resource/:resourceId/roles/add", verifyApiKeyResourceAccess, diff --git a/server/routers/policy/index.ts b/server/routers/policy/index.ts index 9ad10eb45..9d191af15 100644 --- a/server/routers/policy/index.ts +++ b/server/routers/policy/index.ts @@ -1,3 +1,6 @@ export * from "./getResourcePolicy"; export * from "./updateResourcePolicy"; export * from "./setResourcePolicyAccessControl"; +export * from "./setResourcePolicyPassword"; +export * from "./setResourcePolicyPincode"; +export * from "./setResourcePolicyHeaderAuth"; diff --git a/server/routers/policy/setResourcePolicyHeaderAuth.ts b/server/routers/policy/setResourcePolicyHeaderAuth.ts new file mode 100644 index 000000000..3e97a7d00 --- /dev/null +++ b/server/routers/policy/setResourcePolicyHeaderAuth.ts @@ -0,0 +1,130 @@ +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { + db, + resourcePolicyHeaderAuth, + resourcePolicyHeaderAuthExtendedCompatibility +} from "@server/db"; +import { eq } from "drizzle-orm"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import { fromError } from "zod-validation-error"; +import { response } from "@server/lib/response"; +import logger from "@server/logger"; +import { hashPassword } from "@server/auth/password"; +import { OpenAPITags, registry } from "@server/openApi"; + +const setResourcePolicyHeaderAuthParamsSchema = z.object({ + resourcePolicyId: z.string().transform(Number).pipe(z.int().positive()) +}); + +const setResourcePolicyHeaderAuthBodySchema = z.strictObject({ + user: z.string().min(4).max(100).nullable(), + password: z.string().min(4).max(100).nullable(), + extendedCompatibility: z.boolean().nullable() +}); + +registry.registerPath({ + method: "post", + path: "/resource-policy/{resourcePolicyId}/header-auth", + description: + "Set or update the header authentication for a resource policy. If user and password is not provided, it will remove the header authentication.", + tags: [OpenAPITags.Resource], + request: { + params: setResourcePolicyHeaderAuthParamsSchema, + body: { + content: { + "application/json": { + schema: setResourcePolicyHeaderAuthBodySchema + } + } + } + }, + responses: {} +}); + +export async function setResourcePolicyHeaderAuth( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedParams = setResourcePolicyHeaderAuthParamsSchema.safeParse( + req.params + ); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const parsedBody = setResourcePolicyHeaderAuthBodySchema.safeParse( + req.body + ); + if (!parsedBody.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedBody.error).toString() + ) + ); + } + + const { resourcePolicyId } = parsedParams.data; + const { user, password, extendedCompatibility } = parsedBody.data; + + await db.transaction(async (trx) => { + await trx + .delete(resourcePolicyHeaderAuth) + .where( + eq( + resourcePolicyHeaderAuth.resourcePolicyId, + resourcePolicyId + ) + ); + await trx + .delete(resourcePolicyHeaderAuthExtendedCompatibility) + .where( + eq( + resourcePolicyHeaderAuthExtendedCompatibility.resourcePolicyId, + resourcePolicyId + ) + ); + + if (user && password && extendedCompatibility !== null) { + const headerAuthHash = await hashPassword( + Buffer.from(`${user}:${password}`).toString("base64") + ); + + await Promise.all([ + trx + .insert(resourcePolicyHeaderAuth) + .values({ resourcePolicyId, headerAuthHash }), + trx + .insert(resourcePolicyHeaderAuthExtendedCompatibility) + .values({ + resourcePolicyId, + extendedCompatibilityIsActivated: + extendedCompatibility + }) + ]); + } + }); + + return response(res, { + data: {}, + success: true, + error: false, + message: "Header Authentication set successfully", + status: HttpCode.CREATED + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/server/routers/policy/setResourcePolicyPassword.ts b/server/routers/policy/setResourcePolicyPassword.ts new file mode 100644 index 000000000..c7559b75e --- /dev/null +++ b/server/routers/policy/setResourcePolicyPassword.ts @@ -0,0 +1,106 @@ +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db } from "@server/db"; +import { resourcePolicyPassword } from "@server/db"; +import { eq } from "drizzle-orm"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import { fromError } from "zod-validation-error"; +import { response } from "@server/lib/response"; +import logger from "@server/logger"; +import { hashPassword } from "@server/auth/password"; +import { OpenAPITags, registry } from "@server/openApi"; + +const setResourcePolicyPasswordParamsSchema = z.object({ + resourcePolicyId: z.string().transform(Number).pipe(z.int().positive()) +}); + +const setResourcePolicyPasswordBodySchema = z.strictObject({ + password: z.string().min(4).max(100).nullable() +}); + +registry.registerPath({ + method: "post", + path: "/resource-policy/{resourcePolicyId}/password", + description: + "Set the password for a resource policy. Setting the password to null will remove it.", + tags: [OpenAPITags.Resource], + request: { + params: setResourcePolicyPasswordParamsSchema, + body: { + content: { + "application/json": { + schema: setResourcePolicyPasswordBodySchema + } + } + } + }, + responses: {} +}); + +export async function setResourcePolicyPassword( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedParams = setResourcePolicyPasswordParamsSchema.safeParse( + req.params + ); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const parsedBody = setResourcePolicyPasswordBodySchema.safeParse( + req.body + ); + if (!parsedBody.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedBody.error).toString() + ) + ); + } + + const { resourcePolicyId } = parsedParams.data; + const { password } = parsedBody.data; + + await db.transaction(async (trx) => { + await trx + .delete(resourcePolicyPassword) + .where( + eq( + resourcePolicyPassword.resourcePolicyId, + resourcePolicyId + ) + ); + + if (password) { + const passwordHash = await hashPassword(password); + + await trx + .insert(resourcePolicyPassword) + .values({ resourcePolicyId, passwordHash }); + } + }); + + return response(res, { + data: {}, + success: true, + error: false, + message: "Resource policy password set successfully", + status: HttpCode.CREATED + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/server/routers/policy/setResourcePolicyPincode.ts b/server/routers/policy/setResourcePolicyPincode.ts new file mode 100644 index 000000000..d16e05c9e --- /dev/null +++ b/server/routers/policy/setResourcePolicyPincode.ts @@ -0,0 +1,109 @@ +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db } from "@server/db"; +import { resourcePolicyPincode } from "@server/db"; +import { eq } from "drizzle-orm"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import { fromError } from "zod-validation-error"; +import { response } from "@server/lib/response"; +import logger from "@server/logger"; +import { hashPassword } from "@server/auth/password"; +import { OpenAPITags, registry } from "@server/openApi"; + +const setResourcePolicyPincodeParamsSchema = z.object({ + resourcePolicyId: z.string().transform(Number).pipe(z.int().positive()) +}); + +const setResourcePolicyPincodeBodySchema = z.strictObject({ + pincode: z + .string() + .regex(/^\d{6}$/) + .or(z.null()) +}); + +registry.registerPath({ + method: "post", + path: "/resource-policy/{resourcePolicyId}/pincode", + description: + "Set the PIN code for a resource policy. Setting the PIN code to null will remove it.", + tags: [OpenAPITags.Resource], + request: { + params: setResourcePolicyPincodeParamsSchema, + body: { + content: { + "application/json": { + schema: setResourcePolicyPincodeBodySchema + } + } + } + }, + responses: {} +}); + +export async function setResourcePolicyPincode( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedParams = setResourcePolicyPincodeParamsSchema.safeParse( + req.params + ); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const parsedBody = setResourcePolicyPincodeBodySchema.safeParse( + req.body + ); + if (!parsedBody.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedBody.error).toString() + ) + ); + } + + const { resourcePolicyId } = parsedParams.data; + const { pincode } = parsedBody.data; + + await db.transaction(async (trx) => { + await trx + .delete(resourcePolicyPincode) + .where( + eq( + resourcePolicyPincode.resourcePolicyId, + resourcePolicyId + ) + ); + + if (pincode) { + const pincodeHash = await hashPassword(pincode); + + await trx + .insert(resourcePolicyPincode) + .values({ resourcePolicyId, pincodeHash, digitLength: 6 }); + } + }); + + return response(res, { + data: {}, + success: true, + error: false, + message: "Resource policy PIN code set successfully", + status: HttpCode.CREATED + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +}