mirror of
https://github.com/fosrl/pangolin.git
synced 2026-07-02 10:34:55 +00:00
standardize permissions in api
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -178,7 +178,8 @@ export enum ActionsEnum {
|
||||
setResourcePolicyPincode = "setResourcePolicyPincode",
|
||||
setResourcePolicyHeaderAuth = "setResourcePolicyHeaderAuth",
|
||||
setResourcePolicyWhitelist = "setResourcePolicyWhitelist",
|
||||
setResourcePolicyRules = "setResourcePolicyRules"
|
||||
setResourcePolicyRules = "setResourcePolicyRules",
|
||||
createOrgWideLauncherView = "createOrgWideLauncherView"
|
||||
}
|
||||
|
||||
export async function checkUserActionPermission(
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user