diff --git a/messages/en-US.json b/messages/en-US.json index ba22ff77..29b7f9f1 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -1052,6 +1052,11 @@ "actionUpdateClient": "Update Client", "actionListClients": "List Clients", "actionGetClient": "Get Client", + "actionCreateSiteResource": "Create Site Resource", + "actionDeleteSiteResource": "Delete Site Resource", + "actionGetSiteResource": "Get Site Resource", + "actionListSiteResources": "List Site Resources", + "actionUpdateSiteResource": "Update Site Resource", "noneSelected": "None selected", "orgNotFound2": "No organizations found.", "searchProgress": "Search...", diff --git a/server/middlewares/integration/index.ts b/server/middlewares/integration/index.ts index 4caf017b..747cddee 100644 --- a/server/middlewares/integration/index.ts +++ b/server/middlewares/integration/index.ts @@ -11,3 +11,4 @@ export * from "./verifyAccessTokenAccess"; export * from "./verifyApiKeyIsRoot"; export * from "./verifyApiKeyApiKeyAccess"; export * from "./verifyApiKeyClientAccess"; +export * from "./verifyApiKeySiteResourceAccess"; \ No newline at end of file diff --git a/server/middlewares/integration/verifyApiKeySiteResourceAccess.ts b/server/middlewares/integration/verifyApiKeySiteResourceAccess.ts new file mode 100644 index 00000000..cba94cd1 --- /dev/null +++ b/server/middlewares/integration/verifyApiKeySiteResourceAccess.ts @@ -0,0 +1,97 @@ +import { Request, Response, NextFunction } from "express"; +import { db } from "@server/db"; +import { siteResources, apiKeyOrg } from "@server/db"; +import { and, eq } from "drizzle-orm"; +import createHttpError from "http-errors"; +import HttpCode from "@server/types/HttpCode"; + +export async function verifyApiKeySiteResourceAccess( + req: Request, + res: Response, + next: NextFunction +) { + try { + const apiKey = req.apiKey; + const siteResourceId = parseInt(req.params.siteResourceId); + const siteId = parseInt(req.params.siteId); + const orgId = req.params.orgId; + + if (!apiKey) { + return next( + createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated") + ); + } + + if (!siteResourceId || !siteId || !orgId) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "Missing required parameters" + ) + ); + } + + if (apiKey.isRoot) { + // Root keys can access any resource in any org + return next(); + } + + // Check if the site resource exists and belongs to the specified site and org + const [siteResource] = await db + .select() + .from(siteResources) + .where(and( + eq(siteResources.siteResourceId, siteResourceId), + eq(siteResources.siteId, siteId), + eq(siteResources.orgId, orgId) + )) + .limit(1); + + if (!siteResource) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + "Site resource not found" + ) + ); + } + + // Verify that the API key has access to the organization + if (!req.apiKeyOrg) { + const apiKeyOrgRes = await db + .select() + .from(apiKeyOrg) + .where( + and( + eq(apiKeyOrg.apiKeyId, apiKey.apiKeyId), + eq(apiKeyOrg.orgId, orgId) + ) + ) + .limit(1); + + if (apiKeyOrgRes.length === 0) { + return next( + createHttpError( + HttpCode.FORBIDDEN, + "Key does not have access to this organization" + ) + ); + } + + req.apiKeyOrg = apiKeyOrgRes[0]; + } + + // Attach the siteResource to the request for use in the next middleware/route + // @ts-ignore - Extending Request type + req.siteResource = siteResource; + + return next(); + } catch (error) { + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "Error verifying site resource access" + ) + ); + } +} diff --git a/server/routers/integration.ts b/server/routers/integration.ts index d2734fd3..cb38e441 100644 --- a/server/routers/integration.ts +++ b/server/routers/integration.ts @@ -9,6 +9,7 @@ import * as client from "./client"; import * as accessToken from "./accessToken"; import * as apiKeys from "./apiKeys"; import * as idp from "./idp"; +import * as siteResource from "./siteResource"; import { verifyApiKey, verifyApiKeyOrgAccess, @@ -22,7 +23,8 @@ import { verifyApiKeyAccessTokenAccess, verifyApiKeyIsRoot, verifyApiKeyClientAccess, - verifyClientsEnabled + verifyClientsEnabled, + verifyApiKeySiteResourceAccess } from "@server/middlewares"; import HttpCode from "@server/types/HttpCode"; import { Router } from "express"; @@ -128,6 +130,69 @@ authenticated.delete( site.deleteSite ); +authenticated.get( + "/org/:orgId/user-resources", + verifyApiKeyOrgAccess, + resource.getUserResources +); +// Site Resource endpoints +authenticated.put( + "/org/:orgId/site/:siteId/resource", + verifyApiKeyOrgAccess, + verifyApiKeySiteAccess, + verifyApiKeyHasAction(ActionsEnum.createSiteResource), + siteResource.createSiteResource +); + +authenticated.get( + "/org/:orgId/site/:siteId/resources", + verifyApiKeyOrgAccess, + verifyApiKeySiteAccess, + verifyApiKeyHasAction(ActionsEnum.listSiteResources), + siteResource.listSiteResources +); + +authenticated.get( + "/org/:orgId/site-resources", + verifyApiKeyOrgAccess, + verifyApiKeyHasAction(ActionsEnum.listSiteResources), + siteResource.listAllSiteResourcesByOrg +); + +authenticated.get( + "/org/:orgId/site/:siteId/resource/:siteResourceId", + verifyApiKeyOrgAccess, + verifyApiKeySiteAccess, + verifyApiKeySiteResourceAccess, + verifyApiKeyHasAction(ActionsEnum.getSiteResource), + siteResource.getSiteResource +); + +authenticated.post( + "/org/:orgId/site/:siteId/resource/:siteResourceId", + verifyApiKeyOrgAccess, + verifyApiKeySiteAccess, + verifyApiKeySiteResourceAccess, + verifyApiKeyHasAction(ActionsEnum.updateSiteResource), + siteResource.updateSiteResource +); + +authenticated.delete( + "/org/:orgId/site/:siteId/resource/:siteResourceId", + verifyApiKeyOrgAccess, + verifyApiKeySiteAccess, + verifyApiKeySiteResourceAccess, + verifyApiKeyHasAction(ActionsEnum.deleteSiteResource), + siteResource.deleteSiteResource +); + +authenticated.put( + "/org/:orgId/resource", + verifyApiKeyOrgAccess, + verifyApiKeyHasAction(ActionsEnum.createResource), + resource.createResource +); + authenticated.put( "/org/:orgId/site/:siteId/resource", verifyApiKeyOrgAccess, diff --git a/src/components/PermissionsSelectBox.tsx b/src/components/PermissionsSelectBox.tsx index 848d116c..a0d34b4b 100644 --- a/src/components/PermissionsSelectBox.tsx +++ b/src/components/PermissionsSelectBox.tsx @@ -51,7 +51,12 @@ function getActionsCategories(root: boolean) { [t('actionSetResourcePassword')]: "setResourcePassword", [t('actionSetResourcePincode')]: "setResourcePincode", [t('actionSetResourceEmailWhitelist')]: "setResourceWhitelist", - [t('actionGetResourceEmailWhitelist')]: "getResourceWhitelist" + [t('actionGetResourceEmailWhitelist')]: "getResourceWhitelist", + [t('actionCreateSiteResource')]: "createSiteResource", + [t('actionDeleteSiteResource')]: "deleteSiteResource", + [t('actionGetSiteResource')]: "getSiteResource", + [t('actionListSiteResources')]: "listSiteResources", + [t('actionUpdateSiteResource')]: "updateSiteResource" }, Target: {