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

View File

@@ -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",

View File

@@ -178,7 +178,8 @@ export enum ActionsEnum {
setResourcePolicyPincode = "setResourcePolicyPincode",
setResourcePolicyHeaderAuth = "setResourcePolicyHeaderAuth",
setResourcePolicyWhitelist = "setResourcePolicyWhitelist",
setResourcePolicyRules = "setResourcePolicyRules"
setResourcePolicyRules = "setResourcePolicyRules",
createOrgWideLauncherView = "createOrgWideLauncherView"
}
export async function checkUserActionPermission(

View File

@@ -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",

View File

@@ -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<any> {
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"
)
);
}

View File

@@ -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<any> {
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,

View File

@@ -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<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(
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);

View File

@@ -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<any> {
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
);

View File

@@ -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<any> {
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
);

View File

@@ -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<any> {
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)

View File

@@ -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<any> {
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"
)
);
}