mirror of
https://github.com/fosrl/pangolin.git
synced 2026-03-27 13:06:37 +00:00
ui improvements
This commit is contained in:
@@ -4,6 +4,7 @@ import { userActions, roleActions } from "@server/db";
|
||||
import { and, eq, inArray } from "drizzle-orm";
|
||||
import createHttpError from "http-errors";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import { getUserOrgRoleIds } from "@server/lib/userOrgRoles";
|
||||
|
||||
export enum ActionsEnum {
|
||||
createOrgUser = "createOrgUser",
|
||||
@@ -54,6 +55,7 @@ export enum ActionsEnum {
|
||||
// listRoleActions = "listRoleActions",
|
||||
addUserRole = "addUserRole",
|
||||
removeUserRole = "removeUserRole",
|
||||
setUserOrgRoles = "setUserOrgRoles",
|
||||
// addUserSite = "addUserSite",
|
||||
// addUserAction = "addUserAction",
|
||||
// removeUserAction = "removeUserAction",
|
||||
@@ -158,9 +160,6 @@ export async function checkUserActionPermission(
|
||||
let userOrgRoleIds = req.userOrgRoleIds;
|
||||
|
||||
if (userOrgRoleIds === undefined) {
|
||||
const { getUserOrgRoleIds } = await import(
|
||||
"@server/lib/userOrgRoles"
|
||||
);
|
||||
userOrgRoleIds = await getUserOrgRoleIds(userId, req.userOrgId!);
|
||||
if (userOrgRoleIds.length === 0) {
|
||||
throw createHttpError(
|
||||
|
||||
@@ -104,48 +104,6 @@ export async function getUserSessionWithUser(
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user organization role (single role; prefer getUserOrgRoleIds + roles for multi-role).
|
||||
* @deprecated Use userOrgRoles table and getUserOrgRoleIds for multi-role support.
|
||||
*/
|
||||
export async function getUserOrgRole(userId: string, orgId: string) {
|
||||
const userOrg = await db
|
||||
.select({
|
||||
userId: userOrgs.userId,
|
||||
orgId: userOrgs.orgId,
|
||||
isOwner: userOrgs.isOwner,
|
||||
autoProvisioned: userOrgs.autoProvisioned
|
||||
})
|
||||
.from(userOrgs)
|
||||
.where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId)))
|
||||
.limit(1);
|
||||
|
||||
if (userOrg.length === 0) return null;
|
||||
|
||||
const [firstRole] = await db
|
||||
.select({
|
||||
roleId: userOrgRoles.roleId,
|
||||
roleName: roles.name
|
||||
})
|
||||
.from(userOrgRoles)
|
||||
.leftJoin(roles, eq(userOrgRoles.roleId, roles.roleId))
|
||||
.where(
|
||||
and(
|
||||
eq(userOrgRoles.userId, userId),
|
||||
eq(userOrgRoles.orgId, orgId)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
return firstRole
|
||||
? {
|
||||
...userOrg[0],
|
||||
roleId: firstRole.roleId,
|
||||
roleName: firstRole.roleName
|
||||
}
|
||||
: { ...userOrg[0], roleId: null, roleName: null };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get role name by role ID (for display).
|
||||
*/
|
||||
|
||||
@@ -17,6 +17,7 @@ export * from "./verifyAccessTokenAccess";
|
||||
export * from "./requestTimeout";
|
||||
export * from "./verifyClientAccess";
|
||||
export * from "./verifyUserHasAction";
|
||||
export * from "./verifyUserCanSetUserOrgRoles";
|
||||
export * from "./verifyUserIsServerAdmin";
|
||||
export * from "./verifyIsLoggedInUser";
|
||||
export * from "./verifyIsLoggedInUser";
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export * from "./verifyApiKey";
|
||||
export * from "./verifyApiKeyOrgAccess";
|
||||
export * from "./verifyApiKeyHasAction";
|
||||
export * from "./verifyApiKeyCanSetUserOrgRoles";
|
||||
export * from "./verifyApiKeySiteAccess";
|
||||
export * from "./verifyApiKeyResourceAccess";
|
||||
export * from "./verifyApiKeyTargetAccess";
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import createHttpError from "http-errors";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import logger from "@server/logger";
|
||||
import { ActionsEnum } from "@server/auth/actions";
|
||||
import { db } from "@server/db";
|
||||
import { apiKeyActions } from "@server/db";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
|
||||
async function apiKeyHasAction(apiKeyId: string, actionId: ActionsEnum) {
|
||||
const [row] = await db
|
||||
.select()
|
||||
.from(apiKeyActions)
|
||||
.where(
|
||||
and(
|
||||
eq(apiKeyActions.apiKeyId, apiKeyId),
|
||||
eq(apiKeyActions.actionId, actionId)
|
||||
)
|
||||
);
|
||||
return !!row;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows setUserOrgRoles on the key, or both addUserRole and removeUserRole.
|
||||
*/
|
||||
export function verifyApiKeyCanSetUserOrgRoles() {
|
||||
return async function (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
if (!req.apiKey) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.UNAUTHORIZED,
|
||||
"API Key not authenticated"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const keyId = req.apiKey.apiKeyId;
|
||||
|
||||
if (await apiKeyHasAction(keyId, ActionsEnum.setUserOrgRoles)) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const hasAdd = await apiKeyHasAction(keyId, ActionsEnum.addUserRole);
|
||||
const hasRemove = await apiKeyHasAction(
|
||||
keyId,
|
||||
ActionsEnum.removeUserRole
|
||||
);
|
||||
|
||||
if (hasAdd && hasRemove) {
|
||||
return next();
|
||||
}
|
||||
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"Key does not have permission perform this action"
|
||||
)
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error("Error verifying API key set user org roles:", error);
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"Error verifying key action access"
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
54
server/middlewares/verifyUserCanSetUserOrgRoles.ts
Normal file
54
server/middlewares/verifyUserCanSetUserOrgRoles.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import createHttpError from "http-errors";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import logger from "@server/logger";
|
||||
import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
|
||||
|
||||
/**
|
||||
* Allows the new setUserOrgRoles action, or legacy permission pair addUserRole + removeUserRole.
|
||||
*/
|
||||
export function verifyUserCanSetUserOrgRoles() {
|
||||
return async function (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const canSet = await checkUserActionPermission(
|
||||
ActionsEnum.setUserOrgRoles,
|
||||
req
|
||||
);
|
||||
if (canSet) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const canAdd = await checkUserActionPermission(
|
||||
ActionsEnum.addUserRole,
|
||||
req
|
||||
);
|
||||
const canRemove = await checkUserActionPermission(
|
||||
ActionsEnum.removeUserRole,
|
||||
req
|
||||
);
|
||||
|
||||
if (canAdd && canRemove) {
|
||||
return next();
|
||||
}
|
||||
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"User does not have permission perform this action"
|
||||
)
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error("Error verifying set user org roles access:", error);
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"Error verifying role access"
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -39,6 +39,7 @@ import {
|
||||
verifyApiKeyAccess,
|
||||
verifyDomainAccess,
|
||||
verifyUserHasAction,
|
||||
verifyUserCanSetUserOrgRoles,
|
||||
verifyUserIsOrgOwner,
|
||||
verifySiteResourceAccess,
|
||||
verifyOlmAccess,
|
||||
@@ -837,6 +838,16 @@ authenticated.post(
|
||||
user.updateOrgUser
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
"/org/:orgId/user/:userId/roles",
|
||||
verifyOrgAccess,
|
||||
verifyUserAccess,
|
||||
verifyLimits,
|
||||
verifyUserCanSetUserOrgRoles(),
|
||||
logActionAudit(ActionsEnum.setUserOrgRoles),
|
||||
user.setUserOrgRoles
|
||||
);
|
||||
|
||||
authenticated.get("/org/:orgId/user/:userId", verifyOrgAccess, user.getOrgUser);
|
||||
authenticated.get("/org/:orgId/user/:userId/check", org.checkOrgUserAccess);
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
verifyApiKey,
|
||||
verifyApiKeyOrgAccess,
|
||||
verifyApiKeyHasAction,
|
||||
verifyApiKeyCanSetUserOrgRoles,
|
||||
verifyApiKeySiteAccess,
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyApiKeyTargetAccess,
|
||||
@@ -814,6 +815,16 @@ authenticated.post(
|
||||
user.updateOrgUser
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
"/org/:orgId/user/:userId/roles",
|
||||
verifyApiKeyOrgAccess,
|
||||
verifyApiKeyUserAccess,
|
||||
verifyLimits,
|
||||
verifyApiKeyCanSetUserOrgRoles(),
|
||||
logActionAudit(ActionsEnum.setUserOrgRoles),
|
||||
user.setUserOrgRoles
|
||||
);
|
||||
|
||||
authenticated.delete(
|
||||
"/org/:orgId/user/:userId",
|
||||
verifyApiKeyOrgAccess,
|
||||
|
||||
@@ -3,6 +3,7 @@ export * from "./removeUserOrg";
|
||||
export * from "./listUsers";
|
||||
export * from "./addUserRole";
|
||||
export * from "./removeUserRole";
|
||||
export * from "./setUserOrgRoles";
|
||||
export * from "./inviteUser";
|
||||
export * from "./acceptInvite";
|
||||
export * from "./getOrgUser";
|
||||
|
||||
@@ -5,11 +5,10 @@ import { idp, roles, userOrgRoles, userOrgs, users } from "@server/db";
|
||||
import response from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
import { sql } from "drizzle-orm";
|
||||
import { and, eq, inArray, sql } from "drizzle-orm";
|
||||
import logger from "@server/logger";
|
||||
import { fromZodError } from "zod-validation-error";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
const listUsersParamsSchema = z.strictObject({
|
||||
orgId: z.string()
|
||||
@@ -56,15 +55,24 @@ async function queryUsers(orgId: string, limit: number, offset: number) {
|
||||
.limit(limit)
|
||||
.offset(offset);
|
||||
|
||||
const roleRows = await db
|
||||
.select({
|
||||
userId: userOrgRoles.userId,
|
||||
roleId: userOrgRoles.roleId,
|
||||
roleName: roles.name
|
||||
})
|
||||
.from(userOrgRoles)
|
||||
.leftJoin(roles, eq(userOrgRoles.roleId, roles.roleId))
|
||||
.where(eq(userOrgRoles.orgId, orgId));
|
||||
const userIds = rows.map((r) => r.id);
|
||||
const roleRows =
|
||||
userIds.length === 0
|
||||
? []
|
||||
: await db
|
||||
.select({
|
||||
userId: userOrgRoles.userId,
|
||||
roleId: userOrgRoles.roleId,
|
||||
roleName: roles.name
|
||||
})
|
||||
.from(userOrgRoles)
|
||||
.leftJoin(roles, eq(userOrgRoles.roleId, roles.roleId))
|
||||
.where(
|
||||
and(
|
||||
eq(userOrgRoles.orgId, orgId),
|
||||
inArray(userOrgRoles.userId, userIds)
|
||||
)
|
||||
);
|
||||
|
||||
const rolesByUser = new Map<
|
||||
string,
|
||||
|
||||
151
server/routers/user/setUserOrgRoles.ts
Normal file
151
server/routers/user/setUserOrgRoles.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { clients, db } from "@server/db";
|
||||
import { userOrgRoles, userOrgs, roles } from "@server/db";
|
||||
import { eq, and, inArray } from "drizzle-orm";
|
||||
import response from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import { rebuildClientAssociationsFromClient } from "@server/lib/rebuildClientAssociations";
|
||||
|
||||
const setUserOrgRolesParamsSchema = z.strictObject({
|
||||
orgId: z.string(),
|
||||
userId: z.string()
|
||||
});
|
||||
|
||||
const setUserOrgRolesBodySchema = z.strictObject({
|
||||
roleIds: z.array(z.int().positive()).min(1)
|
||||
});
|
||||
|
||||
export async function setUserOrgRoles(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const parsedParams = setUserOrgRolesParamsSchema.safeParse(req.params);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedParams.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const parsedBody = setUserOrgRolesBodySchema.safeParse(req.body);
|
||||
if (!parsedBody.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedBody.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { orgId, userId } = parsedParams.data;
|
||||
const { roleIds } = parsedBody.data;
|
||||
|
||||
if (req.user && !req.userOrg) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"You do not have access to this organization"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const uniqueRoleIds = [...new Set(roleIds)];
|
||||
|
||||
const [existingUser] = await db
|
||||
.select()
|
||||
.from(userOrgs)
|
||||
.where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId)))
|
||||
.limit(1);
|
||||
|
||||
if (!existingUser) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
"User not found in this organization"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (existingUser.isOwner) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"Cannot change the roles of the owner of the organization"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const orgRoles = await db
|
||||
.select({ roleId: roles.roleId })
|
||||
.from(roles)
|
||||
.where(
|
||||
and(
|
||||
eq(roles.orgId, orgId),
|
||||
inArray(roles.roleId, uniqueRoleIds)
|
||||
)
|
||||
);
|
||||
|
||||
if (orgRoles.length !== uniqueRoleIds.length) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"One or more role IDs are invalid for this organization"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
await db.transaction(async (trx) => {
|
||||
await trx
|
||||
.delete(userOrgRoles)
|
||||
.where(
|
||||
and(
|
||||
eq(userOrgRoles.userId, userId),
|
||||
eq(userOrgRoles.orgId, orgId)
|
||||
)
|
||||
);
|
||||
|
||||
if (uniqueRoleIds.length > 0) {
|
||||
await trx.insert(userOrgRoles).values(
|
||||
uniqueRoleIds.map((roleId) => ({
|
||||
userId,
|
||||
orgId,
|
||||
roleId
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
const orgClients = await trx
|
||||
.select()
|
||||
.from(clients)
|
||||
.where(
|
||||
and(eq(clients.userId, userId), eq(clients.orgId, orgId))
|
||||
);
|
||||
|
||||
for (const orgClient of orgClients) {
|
||||
await rebuildClientAssociationsFromClient(orgClient, trx);
|
||||
}
|
||||
});
|
||||
|
||||
return response(res, {
|
||||
data: { userId, orgId, roleIds: uniqueRoleIds },
|
||||
success: true,
|
||||
error: false,
|
||||
message: "User roles set successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user