diff --git a/messages/en-US.json b/messages/en-US.json index d1486cbe6..ab8e3f7a6 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -1512,6 +1512,11 @@ "actionGetVirtualApiKey": "Get Virtual API Key", "actionListVirtualApiKeys": "List Virtual API Keys", "actionUpdateVirtualApiKey": "Update Virtual API Key", + "actionCreateRedirect": "Create Redirect", + "actionDeleteRedirect": "Delete Redirect", + "actionGetRedirect": "Get Redirect", + "actionListRedirects": "List Redirects", + "actionUpdateRedirect": "Update Redirect", "actionApplyBlueprint": "Apply Blueprint", "actionListBlueprints": "List Blueprints", "actionGetBlueprint": "Get Blueprint", diff --git a/server/index.ts b/server/index.ts index 055dae57b..7e3ed3261 100644 --- a/server/index.ts +++ b/server/index.ts @@ -16,6 +16,7 @@ import { RemoteExitNode, Session, SiteResource, + Redirect, User, UserOrg, VirtualApiKey @@ -96,6 +97,7 @@ declare global { aiModel?: AiModel; aiBudget?: AiBudget; virtualApiKey?: VirtualApiKey; + redirect?: Redirect; orgPolicyAllowed?: boolean; } } diff --git a/server/middlewares/integration/index.ts b/server/middlewares/integration/index.ts index c516ede09..45ca71f7a 100644 --- a/server/middlewares/integration/index.ts +++ b/server/middlewares/integration/index.ts @@ -22,3 +22,4 @@ export * from "./verifyApiKeyAiBudgetAccess"; export * from "./verifyApiKeyResourcePolicyAccess"; export * from "./verifyApiKeySiteProvisioningKeyAccess"; export * from "./verifyVirtualApiKeyAccess"; +export * from "./verifyApiKeyRedirectAccess"; diff --git a/server/middlewares/integration/verifyApiKeyRedirectAccess.ts b/server/middlewares/integration/verifyApiKeyRedirectAccess.ts new file mode 100644 index 000000000..373310da6 --- /dev/null +++ b/server/middlewares/integration/verifyApiKeyRedirectAccess.ts @@ -0,0 +1,116 @@ +import { Request, Response, NextFunction } from "express"; +import { Redirect, apiKeyOrg, db, redirects } from "@server/db"; +import { and, eq } from "drizzle-orm"; +import createHttpError from "http-errors"; +import HttpCode from "@server/types/HttpCode"; +import { getFirstString } from "@server/lib/requestParams"; + +export async function verifyApiKeyRedirectAccess( + req: Request, + res: Response, + next: NextFunction +) { + try { + const apiKey = req.apiKey; + const redirectIdRaw = getFirstString(req.params.redirectId); + const niceId = getFirstString(req.params.niceId); + const orgIdParam = getFirstString(req.params.orgId); + + if (!apiKey) { + return next( + createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated") + ); + } + + if (!orgIdParam) { + return next( + createHttpError(HttpCode.BAD_REQUEST, "Invalid organization ID") + ); + } + + let redirect: Redirect | undefined; + + if (niceId) { + const [redirectRes] = await db + .select() + .from(redirects) + .where( + and( + eq(redirects.niceId, niceId), + eq(redirects.orgId, orgIdParam) + ) + ) + .limit(1); + redirect = redirectRes; + } else { + const redirectId = Number.parseInt(redirectIdRaw ?? "", 10); + + if (Number.isNaN(redirectId)) { + return next( + createHttpError(HttpCode.BAD_REQUEST, "Invalid redirect ID") + ); + } + + const [redirectRes] = await db + .select() + .from(redirects) + .where( + and( + eq(redirects.redirectId, redirectId), + eq(redirects.orgId, orgIdParam) + ) + ) + .limit(1); + redirect = redirectRes; + } + + if (!redirect) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + `Redirect with ID ${redirectIdRaw || niceId} not found` + ) + ); + } + + if (apiKey.isRoot) { + req.redirect = redirect; + return next(); + } + + const orgId = redirect.orgId; + + if (!req.apiKeyOrg || req.apiKeyOrg.orgId !== orgId) { + const apiKeyOrgRes = await db + .select() + .from(apiKeyOrg) + .where( + and( + eq(apiKeyOrg.apiKeyId, apiKey.apiKeyId), + eq(apiKeyOrg.orgId, orgId) + ) + ) + .limit(1); + req.apiKeyOrg = apiKeyOrgRes[0]; + } + + if (!req.apiKeyOrg) { + return next( + createHttpError( + HttpCode.FORBIDDEN, + "Key does not have access to this organization" + ) + ); + } + + req.redirect = redirect; + return next(); + } catch (error) { + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "Error verifying redirect access" + ) + ); + } +} diff --git a/server/routers/integration.ts b/server/routers/integration.ts index bcf5a6a32..bccc1a7d3 100644 --- a/server/routers/integration.ts +++ b/server/routers/integration.ts @@ -16,6 +16,7 @@ import * as siteResource from "./siteResource"; import * as aiProvider from "./aiProvider"; import * as aiBudget from "./aiBudget"; import * as virtualApiKey from "./virtualApiKey"; +import * as redirect from "./redirect"; import { verifyApiKey, verifyApiKeyOrgAccess, @@ -38,6 +39,7 @@ import { verifyApiKeyAiModelAccess, verifyApiKeyAiBudgetAccess, verifyApiKeyVirtualApiKeyAccess, + verifyApiKeyRedirectAccess, verifyUserHasAction } from "@server/middlewares"; import HttpCode from "@server/types/HttpCode"; @@ -1833,3 +1835,52 @@ authenticated.get( verifyApiKeyHasAction(ActionsEnum.listAiBudgets), aiBudget.listAiBudgetsForVirtualApiKey ); + +authenticated.put( + "/org/:orgId/redirect", + verifyApiKeyOrgAccess, + verifyApiKeyHasAction(ActionsEnum.createRedirect), + logActionAudit(ActionsEnum.createRedirect), + redirect.createRedirect +); + +authenticated.get( + "/org/:orgId/redirects", + verifyApiKeyOrgAccess, + verifyApiKeyHasAction(ActionsEnum.listRedirects), + redirect.listRedirects +); + +authenticated.get( + "/org/:orgId/redirects/:redirectId", + verifyApiKeyOrgAccess, + verifyApiKeyRedirectAccess, + verifyApiKeyHasAction(ActionsEnum.getRedirect), + redirect.getRedirect +); + +authenticated.get( + "/org/:orgId/redirect/:niceId", + verifyApiKeyOrgAccess, + verifyApiKeyRedirectAccess, + verifyApiKeyHasAction(ActionsEnum.getRedirect), + redirect.getRedirect +); + +authenticated.post( + "/org/:orgId/redirects/:redirectId", + verifyApiKeyOrgAccess, + verifyApiKeyRedirectAccess, + verifyApiKeyHasAction(ActionsEnum.updateRedirect), + logActionAudit(ActionsEnum.updateRedirect), + redirect.updateRedirect +); + +authenticated.delete( + "/org/:orgId/redirects/:redirectId", + verifyApiKeyOrgAccess, + verifyApiKeyRedirectAccess, + verifyApiKeyHasAction(ActionsEnum.deleteRedirect), + logActionAudit(ActionsEnum.deleteRedirect), + redirect.deleteRedirect +); diff --git a/src/components/PermissionsSelectBox.tsx b/src/components/PermissionsSelectBox.tsx index 64e86c39e..499dc2777 100644 --- a/src/components/PermissionsSelectBox.tsx +++ b/src/components/PermissionsSelectBox.tsx @@ -49,6 +49,14 @@ function getActionsCategories(root: boolean) { [t("actionRestartOrgDomain")]: "restartOrgDomain" }, + Redirect: { + [t("actionCreateRedirect")]: "createRedirect", + [t("actionDeleteRedirect")]: "deleteRedirect", + [t("actionGetRedirect")]: "getRedirect", + [t("actionListRedirects")]: "listRedirects", + [t("actionUpdateRedirect")]: "updateRedirect" + }, + Site: { [t("actionCreateSite")]: "createSite", [t("actionDeleteSite")]: "deleteSite",