diff --git a/messages/en-US.json b/messages/en-US.json index be9fe0d3d..3663c8c59 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -1401,6 +1401,7 @@ "actionApplyBlueprint": "Apply Blueprint", "actionListBlueprints": "List Blueprints", "actionGetBlueprint": "Get Blueprint", + "actionCreateOrgWideLauncherView": "Create Org-Wide Launcher View", "setupToken": "Setup Token", "setupTokenDescription": "Enter the setup token from the server console.", "setupTokenRequired": "Setup token is required", diff --git a/server/auth/actions.ts b/server/auth/actions.ts index 71fb33156..093013e6d 100644 --- a/server/auth/actions.ts +++ b/server/auth/actions.ts @@ -178,7 +178,8 @@ export enum ActionsEnum { setResourcePolicyPincode = "setResourcePolicyPincode", setResourcePolicyHeaderAuth = "setResourcePolicyHeaderAuth", setResourcePolicyWhitelist = "setResourcePolicyWhitelist", - setResourcePolicyRules = "setResourcePolicyRules" + setResourcePolicyRules = "setResourcePolicyRules", + createOrgWideLauncherView = "createOrgWideLauncherView" } export async function checkUserActionPermission( diff --git a/server/routers/integration.ts b/server/routers/integration.ts index 0bbe12163..d124263d5 100644 --- a/server/routers/integration.ts +++ b/server/routers/integration.ts @@ -13,7 +13,6 @@ import * as apiKeys from "./apiKeys"; import * as idp from "./idp"; import * as logs from "./auditLogs"; import * as siteResource from "./siteResource"; -import * as launcher from "./launcher"; import { verifyApiKey, verifyApiKeyOrgAccess, @@ -161,41 +160,6 @@ authenticated.get( resource.getUserResources ); -authenticated.get( - "/org/:orgId/launcher/groups", - verifyApiKeyOrgAccess, - launcher.listLauncherGroups -); - -authenticated.get( - "/org/:orgId/launcher/resources", - verifyApiKeyOrgAccess, - launcher.listLauncherResources -); - -authenticated.get( - "/org/:orgId/launcher/views", - verifyApiKeyOrgAccess, - launcher.listLauncherViews -); - -authenticated.post( - "/org/:orgId/launcher/views", - verifyApiKeyOrgAccess, - launcher.createLauncherView -); - -authenticated.put( - "/org/:orgId/launcher/views/:viewId", - verifyApiKeyOrgAccess, - launcher.updateLauncherView -); - -authenticated.delete( - "/org/:orgId/launcher/views/:viewId", - verifyApiKeyOrgAccess, - launcher.deleteLauncherView -); // Site Resource endpoints authenticated.put( "/org/:orgId/site-resource", diff --git a/server/routers/launcher/createLauncherView.ts b/server/routers/launcher/createLauncherView.ts index fb7ff2630..593bad304 100644 --- a/server/routers/launcher/createLauncherView.ts +++ b/server/routers/launcher/createLauncherView.ts @@ -1,17 +1,12 @@ import { db, launcherViews } from "@server/db"; import { response } from "@server/lib/response"; -import { getFirstString } from "@server/lib/requestParams"; import HttpCode from "@server/types/HttpCode"; -import { and, eq } from "drizzle-orm"; import { NextFunction, Request, Response } from "express"; import createHttpError from "http-errors"; import moment from "moment"; import { fromZodError } from "zod-validation-error"; import { z } from "zod"; -import { - isOrgAdminOrOwner, - verifyLauncherOrgMembership -} from "./launcherResourceAccess"; +import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions"; import { launcherViewConfigSchema } from "./types"; const createLauncherViewBodySchema = z.strictObject({ @@ -26,14 +21,8 @@ export async function createLauncherView( next: NextFunction ): Promise { try { - const orgId = getFirstString(req.params.orgId); - const userId = req.user?.userId; - - if (!userId) { - return next( - createHttpError(HttpCode.UNAUTHORIZED, "User not authenticated") - ); - } + const orgId = req.userOrgId; + const userId = req.user!.userId; if (!orgId) { return next( @@ -51,22 +40,16 @@ export async function createLauncherView( ); } - const { userRoleIds } = await verifyLauncherOrgMembership( - orgId, - userId - ); - if (parsed.data.orgWide) { - const canManageOrgWide = await isOrgAdminOrOwner( - orgId, - userId, - userRoleIds + const canCreateOrgWide = await checkUserActionPermission( + ActionsEnum.createOrgWideLauncherView, + req ); - if (!canManageOrgWide) { + if (!canCreateOrgWide) { return next( createHttpError( HttpCode.FORBIDDEN, - "Only administrators can create org-wide views" + "User does not have permission perform this action" ) ); } diff --git a/server/routers/launcher/deleteLauncherView.ts b/server/routers/launcher/deleteLauncherView.ts index c68c6530c..beaf1b433 100644 --- a/server/routers/launcher/deleteLauncherView.ts +++ b/server/routers/launcher/deleteLauncherView.ts @@ -5,10 +5,7 @@ import HttpCode from "@server/types/HttpCode"; import { and, eq } from "drizzle-orm"; import { NextFunction, Request, Response } from "express"; import createHttpError from "http-errors"; -import { - isOrgAdminOrOwner, - verifyLauncherOrgMembership -} from "./launcherResourceAccess"; +import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions"; export async function deleteLauncherView( req: Request, @@ -16,18 +13,12 @@ export async function deleteLauncherView( next: NextFunction ): Promise { try { - const orgId = getFirstString(req.params.orgId); + const orgId = req.userOrgId; + const userId = req.user!.userId; const viewId = Number.parseInt( getFirstString(req.params.viewId) ?? "", 10 ); - const userId = req.user?.userId; - - if (!userId) { - return next( - createHttpError(HttpCode.UNAUTHORIZED, "User not authenticated") - ); - } if (!orgId || !Number.isFinite(viewId)) { return next( @@ -38,11 +29,6 @@ export async function deleteLauncherView( ); } - const { userRoleIds } = await verifyLauncherOrgMembership( - orgId, - userId - ); - const [existing] = await db .select() .from(launcherViews) @@ -62,9 +48,12 @@ export async function deleteLauncherView( const isPersonalView = existing.userId === userId; const isOrgWideView = existing.userId == null; - const isAdmin = await isOrgAdminOrOwner(orgId, userId, userRoleIds); + const canManageOrgWide = await checkUserActionPermission( + ActionsEnum.createOrgWideLauncherView, + req + ); - if (!isPersonalView && !(isOrgWideView && isAdmin)) { + if (!isPersonalView && !(isOrgWideView && canManageOrgWide)) { return next( createHttpError( HttpCode.FORBIDDEN, diff --git a/server/routers/launcher/launcherResourceAccess.ts b/server/routers/launcher/launcherResourceAccess.ts index 5f4a5dd29..3083c0672 100644 --- a/server/routers/launcher/launcherResourceAccess.ts +++ b/server/routers/launcher/launcherResourceAccess.ts @@ -15,7 +15,6 @@ import { sites, targets, userOrgRoles, - userOrgs, userPolicies, userResources, userSiteResources @@ -33,8 +32,6 @@ import { or, sql } from "drizzle-orm"; -import createHttpError from "http-errors"; -import HttpCode from "@server/types/HttpCode"; import { formatPublicResourceAccess, formatSiteResourceAccess @@ -58,65 +55,6 @@ export type AccessibleIds = { siteResourceIds: number[]; }; -export async function verifyLauncherOrgMembership( - orgId: string, - userId: string -): Promise<{ userRoleIds: number[] }> { - const [userOrg] = await db - .select() - .from(userOrgs) - .where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId))) - .limit(1); - - if (!userOrg) { - throw createHttpError(HttpCode.FORBIDDEN, "User not in organization"); - } - - const userRoleIds = await db - .select({ roleId: userOrgRoles.roleId }) - .from(userOrgRoles) - .where( - and(eq(userOrgRoles.userId, userId), eq(userOrgRoles.orgId, orgId)) - ) - .then((rows) => rows.map((r) => r.roleId)); - - return { userRoleIds }; -} - -export async function isOrgAdminOrOwner( - orgId: string, - userId: string, - userRoleIds: number[] -): Promise { - const [membership] = await db - .select({ isOwner: userOrgs.isOwner }) - .from(userOrgs) - .where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId))) - .limit(1); - - if (membership?.isOwner) { - return true; - } - - if (userRoleIds.length === 0) { - return false; - } - - const adminRoles = await db - .select({ roleId: roles.roleId }) - .from(roles) - .where( - and( - eq(roles.orgId, orgId), - eq(roles.isAdmin, true), - inArray(roles.roleId, userRoleIds) - ) - ) - .limit(1); - - return adminRoles.length > 0; -} - export async function resolveAccessibleIds( orgId: string, userId: string, @@ -826,9 +764,9 @@ async function listLabelGroups( export async function listLauncherGroupsForUser( orgId: string, userId: string, + userRoleIds: number[], query: LauncherListQuery ): Promise<{ groups: LauncherGroup[]; total: number }> { - const { userRoleIds } = await verifyLauncherOrgMembership(orgId, userId); const accessible = await resolveAccessibleIds(orgId, userId, userRoleIds); if (query.groupBy === "label") { @@ -1077,9 +1015,9 @@ function sortLauncherResources( export async function listLauncherResourcesForUser( orgId: string, userId: string, + userRoleIds: number[], query: LauncherListQuery & { groupKey: string } ): Promise<{ resources: LauncherResource[]; total: number }> { - const { userRoleIds } = await verifyLauncherOrgMembership(orgId, userId); const accessible = await resolveAccessibleIds(orgId, userId, userRoleIds); const siteFilterIds = parseIdListParam(query.siteIds); diff --git a/server/routers/launcher/listLauncherGroups.ts b/server/routers/launcher/listLauncherGroups.ts index 69ad1a10d..a57e5864a 100644 --- a/server/routers/launcher/listLauncherGroups.ts +++ b/server/routers/launcher/listLauncherGroups.ts @@ -1,5 +1,4 @@ import { response } from "@server/lib/response"; -import { getFirstString } from "@server/lib/requestParams"; import HttpCode from "@server/types/HttpCode"; import { NextFunction, Request, Response } from "express"; import createHttpError from "http-errors"; @@ -13,14 +12,8 @@ export async function listLauncherGroups( next: NextFunction ): Promise { try { - const orgId = getFirstString(req.params.orgId); - const userId = req.user?.userId; - - if (!userId) { - return next( - createHttpError(HttpCode.UNAUTHORIZED, "User not authenticated") - ); - } + const orgId = req.userOrgId; + const userId = req.user!.userId; if (!orgId) { return next( @@ -41,6 +34,7 @@ export async function listLauncherGroups( const { groups, total } = await listLauncherGroupsForUser( orgId, userId, + req.userOrgRoleIds ?? [], parsed.data ); diff --git a/server/routers/launcher/listLauncherResources.ts b/server/routers/launcher/listLauncherResources.ts index a4b2be993..94f18b864 100644 --- a/server/routers/launcher/listLauncherResources.ts +++ b/server/routers/launcher/listLauncherResources.ts @@ -1,5 +1,4 @@ import { response } from "@server/lib/response"; -import { getFirstString } from "@server/lib/requestParams"; import HttpCode from "@server/types/HttpCode"; import { NextFunction, Request, Response } from "express"; import createHttpError from "http-errors"; @@ -18,14 +17,8 @@ export async function listLauncherResources( next: NextFunction ): Promise { try { - const orgId = getFirstString(req.params.orgId); - const userId = req.user?.userId; - - if (!userId) { - return next( - createHttpError(HttpCode.UNAUTHORIZED, "User not authenticated") - ); - } + const orgId = req.userOrgId; + const userId = req.user!.userId; if (!orgId) { return next( @@ -46,6 +39,7 @@ export async function listLauncherResources( const { resources, total } = await listLauncherResourcesForUser( orgId, userId, + req.userOrgRoleIds ?? [], parsed.data ); diff --git a/server/routers/launcher/listLauncherViews.ts b/server/routers/launcher/listLauncherViews.ts index 019dbd2da..e176bae46 100644 --- a/server/routers/launcher/listLauncherViews.ts +++ b/server/routers/launcher/listLauncherViews.ts @@ -1,12 +1,10 @@ import { db, launcherViews } from "@server/db"; import { response } from "@server/lib/response"; -import { getFirstString } from "@server/lib/requestParams"; import HttpCode from "@server/types/HttpCode"; import { and, eq, isNull, or } from "drizzle-orm"; import { NextFunction, Request, Response } from "express"; import createHttpError from "http-errors"; import { launcherViewConfigSchema, type LauncherViewRecord } from "./types"; -import { verifyLauncherOrgMembership } from "./launcherResourceAccess"; function mapViewRow( row: typeof launcherViews.$inferSelect @@ -29,14 +27,8 @@ export async function listLauncherViews( next: NextFunction ): Promise { try { - const orgId = getFirstString(req.params.orgId); - const userId = req.user?.userId; - - if (!userId) { - return next( - createHttpError(HttpCode.UNAUTHORIZED, "User not authenticated") - ); - } + const orgId = req.userOrgId; + const userId = req.user!.userId; if (!orgId) { return next( @@ -44,8 +36,6 @@ export async function listLauncherViews( ); } - await verifyLauncherOrgMembership(orgId, userId); - const rows = await db .select() .from(launcherViews) diff --git a/server/routers/launcher/updateLauncherView.ts b/server/routers/launcher/updateLauncherView.ts index 2db865952..9450da153 100644 --- a/server/routers/launcher/updateLauncherView.ts +++ b/server/routers/launcher/updateLauncherView.ts @@ -8,10 +8,7 @@ import createHttpError from "http-errors"; import moment from "moment"; import { fromZodError } from "zod-validation-error"; import { z } from "zod"; -import { - isOrgAdminOrOwner, - verifyLauncherOrgMembership -} from "./launcherResourceAccess"; +import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions"; import { launcherViewConfigSchema } from "./types"; const updateLauncherViewBodySchema = z.strictObject({ @@ -26,18 +23,12 @@ export async function updateLauncherView( next: NextFunction ): Promise { try { - const orgId = getFirstString(req.params.orgId); + const orgId = req.userOrgId; + const userId = req.user!.userId; const viewId = Number.parseInt( getFirstString(req.params.viewId) ?? "", 10 ); - const userId = req.user?.userId; - - if (!userId) { - return next( - createHttpError(HttpCode.UNAUTHORIZED, "User not authenticated") - ); - } if (!orgId || !Number.isFinite(viewId)) { return next( @@ -58,11 +49,6 @@ export async function updateLauncherView( ); } - const { userRoleIds } = await verifyLauncherOrgMembership( - orgId, - userId - ); - const [existing] = await db .select() .from(launcherViews) @@ -82,9 +68,12 @@ export async function updateLauncherView( const isPersonalView = existing.userId === userId; const isOrgWideView = existing.userId == null; - const isAdmin = await isOrgAdminOrOwner(orgId, userId, userRoleIds); + const canManageOrgWide = await checkUserActionPermission( + ActionsEnum.createOrgWideLauncherView, + req + ); - if (!isPersonalView && !(isOrgWideView && isAdmin)) { + if (!isPersonalView && !(isOrgWideView && canManageOrgWide)) { return next( createHttpError( HttpCode.FORBIDDEN, @@ -93,20 +82,24 @@ export async function updateLauncherView( ); } - if (parsed.data.orgWide === true && !isAdmin) { + if (parsed.data.orgWide === true && !canManageOrgWide) { return next( createHttpError( HttpCode.FORBIDDEN, - "Only administrators can make views org-wide" + "User does not have permission perform this action" ) ); } - if (parsed.data.orgWide === false && isOrgWideView && !isAdmin) { + if ( + parsed.data.orgWide === false && + isOrgWideView && + !canManageOrgWide + ) { return next( createHttpError( HttpCode.FORBIDDEN, - "Only administrators can change org-wide views" + "User does not have permission perform this action" ) ); }