standardize permissions in api

This commit is contained in:
miloschwartz
2026-07-01 11:26:14 -04:00
parent 22dd4220fe
commit 297fd2caf3
10 changed files with 45 additions and 198 deletions
+1
View File
@@ -1401,6 +1401,7 @@
"actionApplyBlueprint": "Apply Blueprint", "actionApplyBlueprint": "Apply Blueprint",
"actionListBlueprints": "List Blueprints", "actionListBlueprints": "List Blueprints",
"actionGetBlueprint": "Get Blueprint", "actionGetBlueprint": "Get Blueprint",
"actionCreateOrgWideLauncherView": "Create Org-Wide Launcher View",
"setupToken": "Setup Token", "setupToken": "Setup Token",
"setupTokenDescription": "Enter the setup token from the server console.", "setupTokenDescription": "Enter the setup token from the server console.",
"setupTokenRequired": "Setup token is required", "setupTokenRequired": "Setup token is required",
+2 -1
View File
@@ -178,7 +178,8 @@ export enum ActionsEnum {
setResourcePolicyPincode = "setResourcePolicyPincode", setResourcePolicyPincode = "setResourcePolicyPincode",
setResourcePolicyHeaderAuth = "setResourcePolicyHeaderAuth", setResourcePolicyHeaderAuth = "setResourcePolicyHeaderAuth",
setResourcePolicyWhitelist = "setResourcePolicyWhitelist", setResourcePolicyWhitelist = "setResourcePolicyWhitelist",
setResourcePolicyRules = "setResourcePolicyRules" setResourcePolicyRules = "setResourcePolicyRules",
createOrgWideLauncherView = "createOrgWideLauncherView"
} }
export async function checkUserActionPermission( export async function checkUserActionPermission(
-36
View File
@@ -13,7 +13,6 @@ import * as apiKeys from "./apiKeys";
import * as idp from "./idp"; import * as idp from "./idp";
import * as logs from "./auditLogs"; import * as logs from "./auditLogs";
import * as siteResource from "./siteResource"; import * as siteResource from "./siteResource";
import * as launcher from "./launcher";
import { import {
verifyApiKey, verifyApiKey,
verifyApiKeyOrgAccess, verifyApiKeyOrgAccess,
@@ -161,41 +160,6 @@ authenticated.get(
resource.getUserResources 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 // Site Resource endpoints
authenticated.put( authenticated.put(
"/org/:orgId/site-resource", "/org/:orgId/site-resource",
+8 -25
View File
@@ -1,17 +1,12 @@
import { db, launcherViews } from "@server/db"; import { db, launcherViews } from "@server/db";
import { response } from "@server/lib/response"; import { response } from "@server/lib/response";
import { getFirstString } from "@server/lib/requestParams";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import { and, eq } from "drizzle-orm";
import { NextFunction, Request, Response } from "express"; import { NextFunction, Request, Response } from "express";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import moment from "moment"; import moment from "moment";
import { fromZodError } from "zod-validation-error"; import { fromZodError } from "zod-validation-error";
import { z } from "zod"; import { z } from "zod";
import { import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
isOrgAdminOrOwner,
verifyLauncherOrgMembership
} from "./launcherResourceAccess";
import { launcherViewConfigSchema } from "./types"; import { launcherViewConfigSchema } from "./types";
const createLauncherViewBodySchema = z.strictObject({ const createLauncherViewBodySchema = z.strictObject({
@@ -26,14 +21,8 @@ export async function createLauncherView(
next: NextFunction next: NextFunction
): Promise<any> { ): Promise<any> {
try { try {
const orgId = getFirstString(req.params.orgId); const orgId = req.userOrgId;
const userId = req.user?.userId; const userId = req.user!.userId;
if (!userId) {
return next(
createHttpError(HttpCode.UNAUTHORIZED, "User not authenticated")
);
}
if (!orgId) { if (!orgId) {
return next( return next(
@@ -51,22 +40,16 @@ export async function createLauncherView(
); );
} }
const { userRoleIds } = await verifyLauncherOrgMembership(
orgId,
userId
);
if (parsed.data.orgWide) { if (parsed.data.orgWide) {
const canManageOrgWide = await isOrgAdminOrOwner( const canCreateOrgWide = await checkUserActionPermission(
orgId, ActionsEnum.createOrgWideLauncherView,
userId, req
userRoleIds
); );
if (!canManageOrgWide) { if (!canCreateOrgWide) {
return next( return next(
createHttpError( createHttpError(
HttpCode.FORBIDDEN, HttpCode.FORBIDDEN,
"Only administrators can create org-wide views" "User does not have permission perform this action"
) )
); );
} }
+8 -19
View File
@@ -5,10 +5,7 @@ import HttpCode from "@server/types/HttpCode";
import { and, eq } from "drizzle-orm"; import { and, eq } from "drizzle-orm";
import { NextFunction, Request, Response } from "express"; import { NextFunction, Request, Response } from "express";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import { import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
isOrgAdminOrOwner,
verifyLauncherOrgMembership
} from "./launcherResourceAccess";
export async function deleteLauncherView( export async function deleteLauncherView(
req: Request, req: Request,
@@ -16,18 +13,12 @@ export async function deleteLauncherView(
next: NextFunction next: NextFunction
): Promise<any> { ): Promise<any> {
try { try {
const orgId = getFirstString(req.params.orgId); const orgId = req.userOrgId;
const userId = req.user!.userId;
const viewId = Number.parseInt( const viewId = Number.parseInt(
getFirstString(req.params.viewId) ?? "", getFirstString(req.params.viewId) ?? "",
10 10
); );
const userId = req.user?.userId;
if (!userId) {
return next(
createHttpError(HttpCode.UNAUTHORIZED, "User not authenticated")
);
}
if (!orgId || !Number.isFinite(viewId)) { if (!orgId || !Number.isFinite(viewId)) {
return next( return next(
@@ -38,11 +29,6 @@ export async function deleteLauncherView(
); );
} }
const { userRoleIds } = await verifyLauncherOrgMembership(
orgId,
userId
);
const [existing] = await db const [existing] = await db
.select() .select()
.from(launcherViews) .from(launcherViews)
@@ -62,9 +48,12 @@ export async function deleteLauncherView(
const isPersonalView = existing.userId === userId; const isPersonalView = existing.userId === userId;
const isOrgWideView = existing.userId == null; 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( return next(
createHttpError( createHttpError(
HttpCode.FORBIDDEN, HttpCode.FORBIDDEN,
@@ -15,7 +15,6 @@ import {
sites, sites,
targets, targets,
userOrgRoles, userOrgRoles,
userOrgs,
userPolicies, userPolicies,
userResources, userResources,
userSiteResources userSiteResources
@@ -33,8 +32,6 @@ import {
or, or,
sql sql
} from "drizzle-orm"; } from "drizzle-orm";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
import { import {
formatPublicResourceAccess, formatPublicResourceAccess,
formatSiteResourceAccess formatSiteResourceAccess
@@ -58,65 +55,6 @@ export type AccessibleIds = {
siteResourceIds: number[]; 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<boolean> {
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( export async function resolveAccessibleIds(
orgId: string, orgId: string,
userId: string, userId: string,
@@ -826,9 +764,9 @@ async function listLabelGroups(
export async function listLauncherGroupsForUser( export async function listLauncherGroupsForUser(
orgId: string, orgId: string,
userId: string, userId: string,
userRoleIds: number[],
query: LauncherListQuery query: LauncherListQuery
): Promise<{ groups: LauncherGroup[]; total: number }> { ): Promise<{ groups: LauncherGroup[]; total: number }> {
const { userRoleIds } = await verifyLauncherOrgMembership(orgId, userId);
const accessible = await resolveAccessibleIds(orgId, userId, userRoleIds); const accessible = await resolveAccessibleIds(orgId, userId, userRoleIds);
if (query.groupBy === "label") { if (query.groupBy === "label") {
@@ -1077,9 +1015,9 @@ function sortLauncherResources(
export async function listLauncherResourcesForUser( export async function listLauncherResourcesForUser(
orgId: string, orgId: string,
userId: string, userId: string,
userRoleIds: number[],
query: LauncherListQuery & { groupKey: string } query: LauncherListQuery & { groupKey: string }
): Promise<{ resources: LauncherResource[]; total: number }> { ): Promise<{ resources: LauncherResource[]; total: number }> {
const { userRoleIds } = await verifyLauncherOrgMembership(orgId, userId);
const accessible = await resolveAccessibleIds(orgId, userId, userRoleIds); const accessible = await resolveAccessibleIds(orgId, userId, userRoleIds);
const siteFilterIds = parseIdListParam(query.siteIds); const siteFilterIds = parseIdListParam(query.siteIds);
@@ -1,5 +1,4 @@
import { response } from "@server/lib/response"; import { response } from "@server/lib/response";
import { getFirstString } from "@server/lib/requestParams";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import { NextFunction, Request, Response } from "express"; import { NextFunction, Request, Response } from "express";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
@@ -13,14 +12,8 @@ export async function listLauncherGroups(
next: NextFunction next: NextFunction
): Promise<any> { ): Promise<any> {
try { try {
const orgId = getFirstString(req.params.orgId); const orgId = req.userOrgId;
const userId = req.user?.userId; const userId = req.user!.userId;
if (!userId) {
return next(
createHttpError(HttpCode.UNAUTHORIZED, "User not authenticated")
);
}
if (!orgId) { if (!orgId) {
return next( return next(
@@ -41,6 +34,7 @@ export async function listLauncherGroups(
const { groups, total } = await listLauncherGroupsForUser( const { groups, total } = await listLauncherGroupsForUser(
orgId, orgId,
userId, userId,
req.userOrgRoleIds ?? [],
parsed.data parsed.data
); );
@@ -1,5 +1,4 @@
import { response } from "@server/lib/response"; import { response } from "@server/lib/response";
import { getFirstString } from "@server/lib/requestParams";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import { NextFunction, Request, Response } from "express"; import { NextFunction, Request, Response } from "express";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
@@ -18,14 +17,8 @@ export async function listLauncherResources(
next: NextFunction next: NextFunction
): Promise<any> { ): Promise<any> {
try { try {
const orgId = getFirstString(req.params.orgId); const orgId = req.userOrgId;
const userId = req.user?.userId; const userId = req.user!.userId;
if (!userId) {
return next(
createHttpError(HttpCode.UNAUTHORIZED, "User not authenticated")
);
}
if (!orgId) { if (!orgId) {
return next( return next(
@@ -46,6 +39,7 @@ export async function listLauncherResources(
const { resources, total } = await listLauncherResourcesForUser( const { resources, total } = await listLauncherResourcesForUser(
orgId, orgId,
userId, userId,
req.userOrgRoleIds ?? [],
parsed.data parsed.data
); );
+2 -12
View File
@@ -1,12 +1,10 @@
import { db, launcherViews } from "@server/db"; import { db, launcherViews } from "@server/db";
import { response } from "@server/lib/response"; import { response } from "@server/lib/response";
import { getFirstString } from "@server/lib/requestParams";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import { and, eq, isNull, or } from "drizzle-orm"; import { and, eq, isNull, or } from "drizzle-orm";
import { NextFunction, Request, Response } from "express"; import { NextFunction, Request, Response } from "express";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import { launcherViewConfigSchema, type LauncherViewRecord } from "./types"; import { launcherViewConfigSchema, type LauncherViewRecord } from "./types";
import { verifyLauncherOrgMembership } from "./launcherResourceAccess";
function mapViewRow( function mapViewRow(
row: typeof launcherViews.$inferSelect row: typeof launcherViews.$inferSelect
@@ -29,14 +27,8 @@ export async function listLauncherViews(
next: NextFunction next: NextFunction
): Promise<any> { ): Promise<any> {
try { try {
const orgId = getFirstString(req.params.orgId); const orgId = req.userOrgId;
const userId = req.user?.userId; const userId = req.user!.userId;
if (!userId) {
return next(
createHttpError(HttpCode.UNAUTHORIZED, "User not authenticated")
);
}
if (!orgId) { if (!orgId) {
return next( return next(
@@ -44,8 +36,6 @@ export async function listLauncherViews(
); );
} }
await verifyLauncherOrgMembership(orgId, userId);
const rows = await db const rows = await db
.select() .select()
.from(launcherViews) .from(launcherViews)
+16 -23
View File
@@ -8,10 +8,7 @@ import createHttpError from "http-errors";
import moment from "moment"; import moment from "moment";
import { fromZodError } from "zod-validation-error"; import { fromZodError } from "zod-validation-error";
import { z } from "zod"; import { z } from "zod";
import { import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
isOrgAdminOrOwner,
verifyLauncherOrgMembership
} from "./launcherResourceAccess";
import { launcherViewConfigSchema } from "./types"; import { launcherViewConfigSchema } from "./types";
const updateLauncherViewBodySchema = z.strictObject({ const updateLauncherViewBodySchema = z.strictObject({
@@ -26,18 +23,12 @@ export async function updateLauncherView(
next: NextFunction next: NextFunction
): Promise<any> { ): Promise<any> {
try { try {
const orgId = getFirstString(req.params.orgId); const orgId = req.userOrgId;
const userId = req.user!.userId;
const viewId = Number.parseInt( const viewId = Number.parseInt(
getFirstString(req.params.viewId) ?? "", getFirstString(req.params.viewId) ?? "",
10 10
); );
const userId = req.user?.userId;
if (!userId) {
return next(
createHttpError(HttpCode.UNAUTHORIZED, "User not authenticated")
);
}
if (!orgId || !Number.isFinite(viewId)) { if (!orgId || !Number.isFinite(viewId)) {
return next( return next(
@@ -58,11 +49,6 @@ export async function updateLauncherView(
); );
} }
const { userRoleIds } = await verifyLauncherOrgMembership(
orgId,
userId
);
const [existing] = await db const [existing] = await db
.select() .select()
.from(launcherViews) .from(launcherViews)
@@ -82,9 +68,12 @@ export async function updateLauncherView(
const isPersonalView = existing.userId === userId; const isPersonalView = existing.userId === userId;
const isOrgWideView = existing.userId == null; 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( return next(
createHttpError( createHttpError(
HttpCode.FORBIDDEN, HttpCode.FORBIDDEN,
@@ -93,20 +82,24 @@ export async function updateLauncherView(
); );
} }
if (parsed.data.orgWide === true && !isAdmin) { if (parsed.data.orgWide === true && !canManageOrgWide) {
return next( return next(
createHttpError( createHttpError(
HttpCode.FORBIDDEN, 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( return next(
createHttpError( createHttpError(
HttpCode.FORBIDDEN, HttpCode.FORBIDDEN,
"Only administrators can change org-wide views" "User does not have permission perform this action"
) )
); );
} }