mirror of
https://github.com/fosrl/pangolin.git
synced 2026-03-30 02:15:43 +00:00
support legacy one role per user
This commit is contained in:
@@ -512,6 +512,8 @@
|
|||||||
"autoProvisionedDescription": "Allow this user to be automatically managed by identity provider",
|
"autoProvisionedDescription": "Allow this user to be automatically managed by identity provider",
|
||||||
"accessControlsDescription": "Manage what this user can access and do in the organization",
|
"accessControlsDescription": "Manage what this user can access and do in the organization",
|
||||||
"accessControlsSubmit": "Save Access Controls",
|
"accessControlsSubmit": "Save Access Controls",
|
||||||
|
"singleRolePerUserPlanNotice": "Your plan only supports one role per user.",
|
||||||
|
"singleRolePerUserEditionNotice": "This edition only supports one role per user.",
|
||||||
"roles": "Roles",
|
"roles": "Roles",
|
||||||
"accessUsersRoles": "Manage Users & Roles",
|
"accessUsersRoles": "Manage Users & Roles",
|
||||||
"accessUsersRolesDescription": "Invite users and add them to roles to manage access to the organization",
|
"accessUsersRolesDescription": "Invite users and add them to roles to manage access to the organization",
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import * as misc from "#private/routers/misc";
|
|||||||
import * as reKey from "#private/routers/re-key";
|
import * as reKey from "#private/routers/re-key";
|
||||||
import * as approval from "#private/routers/approvals";
|
import * as approval from "#private/routers/approvals";
|
||||||
import * as ssh from "#private/routers/ssh";
|
import * as ssh from "#private/routers/ssh";
|
||||||
|
import * as user from "#private/routers/user";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
verifyOrgAccess,
|
verifyOrgAccess,
|
||||||
@@ -33,7 +34,10 @@ import {
|
|||||||
verifyUserIsServerAdmin,
|
verifyUserIsServerAdmin,
|
||||||
verifySiteAccess,
|
verifySiteAccess,
|
||||||
verifyClientAccess,
|
verifyClientAccess,
|
||||||
verifyLimits
|
verifyLimits,
|
||||||
|
verifyRoleAccess,
|
||||||
|
verifyUserAccess,
|
||||||
|
verifyUserCanSetUserOrgRoles
|
||||||
} from "@server/middlewares";
|
} from "@server/middlewares";
|
||||||
import { ActionsEnum } from "@server/auth/actions";
|
import { ActionsEnum } from "@server/auth/actions";
|
||||||
import {
|
import {
|
||||||
@@ -518,3 +522,33 @@ authenticated.post(
|
|||||||
// logActionAudit(ActionsEnum.signSshKey), // it is handled inside of the function below so we can include more metadata
|
// logActionAudit(ActionsEnum.signSshKey), // it is handled inside of the function below so we can include more metadata
|
||||||
ssh.signSshKey
|
ssh.signSshKey
|
||||||
);
|
);
|
||||||
|
|
||||||
|
authenticated.post(
|
||||||
|
"/user/:userId/add-role/:roleId",
|
||||||
|
verifyRoleAccess,
|
||||||
|
verifyUserAccess,
|
||||||
|
verifyLimits,
|
||||||
|
verifyUserHasAction(ActionsEnum.addUserRole),
|
||||||
|
logActionAudit(ActionsEnum.addUserRole),
|
||||||
|
user.addUserRole
|
||||||
|
);
|
||||||
|
|
||||||
|
authenticated.delete(
|
||||||
|
"/user/:userId/remove-role/:roleId",
|
||||||
|
verifyRoleAccess,
|
||||||
|
verifyUserAccess,
|
||||||
|
verifyLimits,
|
||||||
|
verifyUserHasAction(ActionsEnum.removeUserRole),
|
||||||
|
logActionAudit(ActionsEnum.removeUserRole),
|
||||||
|
user.removeUserRole
|
||||||
|
);
|
||||||
|
|
||||||
|
authenticated.post(
|
||||||
|
"/user/:userId/org/:orgId/roles",
|
||||||
|
verifyOrgAccess,
|
||||||
|
verifyUserAccess,
|
||||||
|
verifyLimits,
|
||||||
|
verifyUserCanSetUserOrgRoles(),
|
||||||
|
logActionAudit(ActionsEnum.setUserOrgRoles),
|
||||||
|
user.setUserOrgRoles
|
||||||
|
);
|
||||||
|
|||||||
@@ -20,8 +20,11 @@ import {
|
|||||||
verifyApiKeyIsRoot,
|
verifyApiKeyIsRoot,
|
||||||
verifyApiKeyOrgAccess,
|
verifyApiKeyOrgAccess,
|
||||||
verifyApiKeyIdpAccess,
|
verifyApiKeyIdpAccess,
|
||||||
|
verifyApiKeyRoleAccess,
|
||||||
|
verifyApiKeyUserAccess,
|
||||||
verifyLimits
|
verifyLimits
|
||||||
} from "@server/middlewares";
|
} from "@server/middlewares";
|
||||||
|
import * as user from "#private/routers/user";
|
||||||
import {
|
import {
|
||||||
verifyValidSubscription,
|
verifyValidSubscription,
|
||||||
verifyValidLicense
|
verifyValidLicense
|
||||||
@@ -140,3 +143,23 @@ authenticated.get(
|
|||||||
verifyApiKeyHasAction(ActionsEnum.listIdps),
|
verifyApiKeyHasAction(ActionsEnum.listIdps),
|
||||||
orgIdp.listOrgIdps
|
orgIdp.listOrgIdps
|
||||||
);
|
);
|
||||||
|
|
||||||
|
authenticated.post(
|
||||||
|
"/user/:userId/add-role/:roleId",
|
||||||
|
verifyApiKeyRoleAccess,
|
||||||
|
verifyApiKeyUserAccess,
|
||||||
|
verifyLimits,
|
||||||
|
verifyApiKeyHasAction(ActionsEnum.addUserRole),
|
||||||
|
logActionAudit(ActionsEnum.addUserRole),
|
||||||
|
user.addUserRole
|
||||||
|
);
|
||||||
|
|
||||||
|
authenticated.delete(
|
||||||
|
"/user/:userId/remove-role/:roleId",
|
||||||
|
verifyApiKeyRoleAccess,
|
||||||
|
verifyApiKeyUserAccess,
|
||||||
|
verifyLimits,
|
||||||
|
verifyApiKeyHasAction(ActionsEnum.removeUserRole),
|
||||||
|
logActionAudit(ActionsEnum.removeUserRole),
|
||||||
|
user.removeUserRole
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,5 +1,19 @@
|
|||||||
|
/*
|
||||||
|
* This file is part of a proprietary work.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2025 Fossorial, Inc.
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* This file is licensed under the Fossorial Commercial License.
|
||||||
|
* You may not use this file except in compliance with the License.
|
||||||
|
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||||
|
*
|
||||||
|
* This file is not licensed under the AGPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import stoi from "@server/lib/stoi";
|
||||||
import { clients, db } from "@server/db";
|
import { clients, db } from "@server/db";
|
||||||
import { userOrgRoles, userOrgs, roles } from "@server/db";
|
import { userOrgRoles, userOrgs, roles } from "@server/db";
|
||||||
import { eq, and } from "drizzle-orm";
|
import { eq, and } from "drizzle-orm";
|
||||||
@@ -8,7 +22,6 @@ import HttpCode from "@server/types/HttpCode";
|
|||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
import stoi from "@server/lib/stoi";
|
|
||||||
import { OpenAPITags, registry } from "@server/openApi";
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
import { rebuildClientAssociationsFromClient } from "@server/lib/rebuildClientAssociations";
|
import { rebuildClientAssociationsFromClient } from "@server/lib/rebuildClientAssociations";
|
||||||
|
|
||||||
@@ -17,11 +30,9 @@ const addUserRoleParamsSchema = z.strictObject({
|
|||||||
roleId: z.string().transform(stoi).pipe(z.number())
|
roleId: z.string().transform(stoi).pipe(z.number())
|
||||||
});
|
});
|
||||||
|
|
||||||
export type AddUserRoleResponse = z.infer<typeof addUserRoleParamsSchema>;
|
|
||||||
|
|
||||||
registry.registerPath({
|
registry.registerPath({
|
||||||
method: "post",
|
method: "post",
|
||||||
path: "/role/{roleId}/add/{userId}",
|
path: "/user/{userId}/add-role/{roleId}",
|
||||||
description: "Add a role to a user.",
|
description: "Add a role to a user.",
|
||||||
tags: [OpenAPITags.Role, OpenAPITags.User],
|
tags: [OpenAPITags.Role, OpenAPITags.User],
|
||||||
request: {
|
request: {
|
||||||
16
server/private/routers/user/index.ts
Normal file
16
server/private/routers/user/index.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
/*
|
||||||
|
* This file is part of a proprietary work.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2025 Fossorial, Inc.
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* This file is licensed under the Fossorial Commercial License.
|
||||||
|
* You may not use this file except in compliance with the License.
|
||||||
|
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||||
|
*
|
||||||
|
* This file is not licensed under the AGPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export * from "./addUserRole";
|
||||||
|
export * from "./removeUserRole";
|
||||||
|
export * from "./setUserOrgRoles";
|
||||||
@@ -1,5 +1,19 @@
|
|||||||
|
/*
|
||||||
|
* This file is part of a proprietary work.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2025 Fossorial, Inc.
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* This file is licensed under the Fossorial Commercial License.
|
||||||
|
* You may not use this file except in compliance with the License.
|
||||||
|
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||||
|
*
|
||||||
|
* This file is not licensed under the AGPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import stoi from "@server/lib/stoi";
|
||||||
import { db } from "@server/db";
|
import { db } from "@server/db";
|
||||||
import { userOrgRoles, userOrgs, roles, clients } from "@server/db";
|
import { userOrgRoles, userOrgs, roles, clients } from "@server/db";
|
||||||
import { eq, and } from "drizzle-orm";
|
import { eq, and } from "drizzle-orm";
|
||||||
@@ -8,7 +22,6 @@ import HttpCode from "@server/types/HttpCode";
|
|||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
import stoi from "@server/lib/stoi";
|
|
||||||
import { OpenAPITags, registry } from "@server/openApi";
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
import { rebuildClientAssociationsFromClient } from "@server/lib/rebuildClientAssociations";
|
import { rebuildClientAssociationsFromClient } from "@server/lib/rebuildClientAssociations";
|
||||||
|
|
||||||
@@ -19,8 +32,9 @@ const removeUserRoleParamsSchema = z.strictObject({
|
|||||||
|
|
||||||
registry.registerPath({
|
registry.registerPath({
|
||||||
method: "delete",
|
method: "delete",
|
||||||
path: "/role/{roleId}/remove/{userId}",
|
path: "/user/{userId}/remove-role/{roleId}",
|
||||||
description: "Remove a role from a user. User must have at least one role left in the org.",
|
description:
|
||||||
|
"Remove a role from a user. User must have at least one role left in the org.",
|
||||||
tags: [OpenAPITags.Role, OpenAPITags.User],
|
tags: [OpenAPITags.Role, OpenAPITags.User],
|
||||||
request: {
|
request: {
|
||||||
params: removeUserRoleParamsSchema
|
params: removeUserRoleParamsSchema
|
||||||
@@ -1,3 +1,16 @@
|
|||||||
|
/*
|
||||||
|
* This file is part of a proprietary work.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2025 Fossorial, Inc.
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* This file is licensed under the Fossorial Commercial License.
|
||||||
|
* You may not use this file except in compliance with the License.
|
||||||
|
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||||
|
*
|
||||||
|
* This file is not licensed under the AGPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { clients, db } from "@server/db";
|
import { clients, db } from "@server/db";
|
||||||
@@ -8,7 +21,6 @@ import HttpCode from "@server/types/HttpCode";
|
|||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
import { OpenAPITags, registry } from "@server/openApi";
|
|
||||||
import { rebuildClientAssociationsFromClient } from "@server/lib/rebuildClientAssociations";
|
import { rebuildClientAssociationsFromClient } from "@server/lib/rebuildClientAssociations";
|
||||||
|
|
||||||
const setUserOrgRolesParamsSchema = z.strictObject({
|
const setUserOrgRolesParamsSchema = z.strictObject({
|
||||||
@@ -39,7 +39,6 @@ import {
|
|||||||
verifyApiKeyAccess,
|
verifyApiKeyAccess,
|
||||||
verifyDomainAccess,
|
verifyDomainAccess,
|
||||||
verifyUserHasAction,
|
verifyUserHasAction,
|
||||||
verifyUserCanSetUserOrgRoles,
|
|
||||||
verifyUserIsOrgOwner,
|
verifyUserIsOrgOwner,
|
||||||
verifySiteResourceAccess,
|
verifySiteResourceAccess,
|
||||||
verifyOlmAccess,
|
verifyOlmAccess,
|
||||||
@@ -645,6 +644,7 @@ authenticated.delete(
|
|||||||
logActionAudit(ActionsEnum.deleteRole),
|
logActionAudit(ActionsEnum.deleteRole),
|
||||||
role.deleteRole
|
role.deleteRole
|
||||||
);
|
);
|
||||||
|
|
||||||
authenticated.post(
|
authenticated.post(
|
||||||
"/role/:roleId/add/:userId",
|
"/role/:roleId/add/:userId",
|
||||||
verifyRoleAccess,
|
verifyRoleAccess,
|
||||||
@@ -652,17 +652,7 @@ authenticated.post(
|
|||||||
verifyLimits,
|
verifyLimits,
|
||||||
verifyUserHasAction(ActionsEnum.addUserRole),
|
verifyUserHasAction(ActionsEnum.addUserRole),
|
||||||
logActionAudit(ActionsEnum.addUserRole),
|
logActionAudit(ActionsEnum.addUserRole),
|
||||||
user.addUserRole
|
user.addUserRoleLegacy
|
||||||
);
|
|
||||||
|
|
||||||
authenticated.delete(
|
|
||||||
"/role/:roleId/remove/:userId",
|
|
||||||
verifyRoleAccess,
|
|
||||||
verifyUserAccess,
|
|
||||||
verifyLimits,
|
|
||||||
verifyUserHasAction(ActionsEnum.removeUserRole),
|
|
||||||
logActionAudit(ActionsEnum.removeUserRole),
|
|
||||||
user.removeUserRole
|
|
||||||
);
|
);
|
||||||
|
|
||||||
authenticated.post(
|
authenticated.post(
|
||||||
@@ -838,16 +828,6 @@ authenticated.post(
|
|||||||
user.updateOrgUser
|
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", verifyOrgAccess, user.getOrgUser);
|
||||||
authenticated.get("/org/:orgId/user/:userId/check", org.checkOrgUserAccess);
|
authenticated.get("/org/:orgId/user/:userId/check", org.checkOrgUserAccess);
|
||||||
|
|
||||||
|
|||||||
@@ -596,17 +596,7 @@ authenticated.post(
|
|||||||
verifyLimits,
|
verifyLimits,
|
||||||
verifyApiKeyHasAction(ActionsEnum.addUserRole),
|
verifyApiKeyHasAction(ActionsEnum.addUserRole),
|
||||||
logActionAudit(ActionsEnum.addUserRole),
|
logActionAudit(ActionsEnum.addUserRole),
|
||||||
user.addUserRole
|
user.addUserRoleLegacy
|
||||||
);
|
|
||||||
|
|
||||||
authenticated.delete(
|
|
||||||
"/role/:roleId/remove/:userId",
|
|
||||||
verifyApiKeyRoleAccess,
|
|
||||||
verifyApiKeyUserAccess,
|
|
||||||
verifyLimits,
|
|
||||||
verifyApiKeyHasAction(ActionsEnum.removeUserRole),
|
|
||||||
logActionAudit(ActionsEnum.removeUserRole),
|
|
||||||
user.removeUserRole
|
|
||||||
);
|
);
|
||||||
|
|
||||||
authenticated.post(
|
authenticated.post(
|
||||||
@@ -815,16 +805,6 @@ authenticated.post(
|
|||||||
user.updateOrgUser
|
user.updateOrgUser
|
||||||
);
|
);
|
||||||
|
|
||||||
authenticated.post(
|
|
||||||
"/org/:orgId/user/:userId/roles",
|
|
||||||
verifyApiKeyOrgAccess,
|
|
||||||
verifyApiKeyUserAccess,
|
|
||||||
verifyLimits,
|
|
||||||
verifyApiKeyCanSetUserOrgRoles(),
|
|
||||||
logActionAudit(ActionsEnum.setUserOrgRoles),
|
|
||||||
user.setUserOrgRoles
|
|
||||||
);
|
|
||||||
|
|
||||||
authenticated.delete(
|
authenticated.delete(
|
||||||
"/org/:orgId/user/:userId",
|
"/org/:orgId/user/:userId",
|
||||||
verifyApiKeyOrgAccess,
|
verifyApiKeyOrgAccess,
|
||||||
|
|||||||
159
server/routers/user/addUserRoleLegacy.ts
Normal file
159
server/routers/user/addUserRoleLegacy.ts
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
import { Request, Response, NextFunction } from "express";
|
||||||
|
import { z } from "zod";
|
||||||
|
import stoi from "@server/lib/stoi";
|
||||||
|
import { clients, db } from "@server/db";
|
||||||
|
import { userOrgRoles, userOrgs, roles } from "@server/db";
|
||||||
|
import { eq, and } 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";
|
||||||
|
|
||||||
|
/** Legacy path param order: /role/:roleId/add/:userId */
|
||||||
|
const addUserRoleLegacyParamsSchema = z.strictObject({
|
||||||
|
roleId: z.string().transform(stoi).pipe(z.number()),
|
||||||
|
userId: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
registry.registerPath({
|
||||||
|
method: "post",
|
||||||
|
path: "/role/{roleId}/add/{userId}",
|
||||||
|
description:
|
||||||
|
"Legacy: set exactly one role for the user (replaces any other roles the user has in the org).",
|
||||||
|
tags: [OpenAPITags.Role, OpenAPITags.User],
|
||||||
|
request: {
|
||||||
|
params: addUserRoleLegacyParamsSchema
|
||||||
|
},
|
||||||
|
responses: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function addUserRoleLegacy(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
): Promise<any> {
|
||||||
|
try {
|
||||||
|
const parsedParams = addUserRoleLegacyParamsSchema.safeParse(
|
||||||
|
req.params
|
||||||
|
);
|
||||||
|
if (!parsedParams.success) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromError(parsedParams.error).toString()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { userId, roleId } = parsedParams.data;
|
||||||
|
|
||||||
|
if (req.user && !req.userOrg) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.FORBIDDEN,
|
||||||
|
"You do not have access to this organization"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [role] = await db
|
||||||
|
.select()
|
||||||
|
.from(roles)
|
||||||
|
.where(eq(roles.roleId, roleId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!role) {
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.BAD_REQUEST, "Invalid role ID")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [existingUser] = await db
|
||||||
|
.select()
|
||||||
|
.from(userOrgs)
|
||||||
|
.where(
|
||||||
|
and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, role.orgId))
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!existingUser) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.NOT_FOUND,
|
||||||
|
"User not found or does not belong to the specified organization"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingUser.isOwner) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.FORBIDDEN,
|
||||||
|
"Cannot change the role of the owner of the organization"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [roleInOrg] = await db
|
||||||
|
.select()
|
||||||
|
.from(roles)
|
||||||
|
.where(and(eq(roles.roleId, roleId), eq(roles.orgId, role.orgId)))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!roleInOrg) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.NOT_FOUND,
|
||||||
|
"Role not found or does not belong to the specified organization"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.transaction(async (trx) => {
|
||||||
|
await trx
|
||||||
|
.delete(userOrgRoles)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(userOrgRoles.userId, userId),
|
||||||
|
eq(userOrgRoles.orgId, role.orgId)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
await trx.insert(userOrgRoles).values({
|
||||||
|
userId,
|
||||||
|
orgId: role.orgId,
|
||||||
|
roleId
|
||||||
|
});
|
||||||
|
|
||||||
|
const orgClients = await trx
|
||||||
|
.select()
|
||||||
|
.from(clients)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(clients.userId, userId),
|
||||||
|
eq(clients.orgId, role.orgId)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const orgClient of orgClients) {
|
||||||
|
await rebuildClientAssociationsFromClient(orgClient, trx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return response(res, {
|
||||||
|
data: { ...existingUser, roleId },
|
||||||
|
success: true,
|
||||||
|
error: false,
|
||||||
|
message: "Role added to user successfully",
|
||||||
|
status: HttpCode.OK
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
export * from "./getUser";
|
export * from "./getUser";
|
||||||
export * from "./removeUserOrg";
|
export * from "./removeUserOrg";
|
||||||
export * from "./listUsers";
|
export * from "./listUsers";
|
||||||
export * from "./addUserRole";
|
export * from "./types";
|
||||||
export * from "./removeUserRole";
|
export * from "./addUserRoleLegacy";
|
||||||
export * from "./setUserOrgRoles";
|
|
||||||
export * from "./inviteUser";
|
export * from "./inviteUser";
|
||||||
export * from "./acceptInvite";
|
export * from "./acceptInvite";
|
||||||
export * from "./getOrgUser";
|
export * from "./getOrgUser";
|
||||||
|
|||||||
18
server/routers/user/types.ts
Normal file
18
server/routers/user/types.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import type { UserOrg } from "@server/db";
|
||||||
|
|
||||||
|
export type AddUserRoleResponse = {
|
||||||
|
userId: string;
|
||||||
|
roleId: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Legacy POST /role/:roleId/add/:userId response shape (membership + effective role). */
|
||||||
|
export type AddUserRoleLegacyResponse = UserOrg & { roleId: number };
|
||||||
|
|
||||||
|
export type SetUserOrgRolesParams = {
|
||||||
|
orgId: string;
|
||||||
|
userId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SetUserOrgRolesBody = {
|
||||||
|
roleIds: number[];
|
||||||
|
};
|
||||||
@@ -37,6 +37,9 @@ import { useEnvContext } from "@app/hooks/useEnvContext";
|
|||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import IdpTypeBadge from "@app/components/IdpTypeBadge";
|
import IdpTypeBadge from "@app/components/IdpTypeBadge";
|
||||||
import { UserType } from "@server/types/UserTypes";
|
import { UserType } from "@server/types/UserTypes";
|
||||||
|
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
||||||
|
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||||
|
import { build } from "@server/build";
|
||||||
|
|
||||||
const accessControlsFormSchema = z.object({
|
const accessControlsFormSchema = z.object({
|
||||||
username: z.string(),
|
username: z.string(),
|
||||||
@@ -51,8 +54,9 @@ const accessControlsFormSchema = z.object({
|
|||||||
|
|
||||||
export default function AccessControlsPage() {
|
export default function AccessControlsPage() {
|
||||||
const { orgUser: user, updateOrgUser } = userOrgUserContext();
|
const { orgUser: user, updateOrgUser } = userOrgUserContext();
|
||||||
|
const { env } = useEnvContext();
|
||||||
|
|
||||||
const api = createApiClient(useEnvContext());
|
const api = createApiClient({ env });
|
||||||
|
|
||||||
const { orgId } = useParams();
|
const { orgId } = useParams();
|
||||||
|
|
||||||
@@ -63,6 +67,18 @@ export default function AccessControlsPage() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
|
const { isPaidUser, hasSaasSubscription, hasEnterpriseLicense } =
|
||||||
|
usePaidStatus();
|
||||||
|
const multiRoleFeatureTiers = Array.from(
|
||||||
|
new Set([...tierMatrix.sshPam, ...tierMatrix.orgOidc])
|
||||||
|
);
|
||||||
|
const isPaid = isPaidUser(multiRoleFeatureTiers);
|
||||||
|
const supportsMultipleRolesPerUser = isPaid;
|
||||||
|
const showMultiRolePaywallMessage =
|
||||||
|
!env.flags.disableEnterpriseFeatures &&
|
||||||
|
((build === "saas" && !isPaid) ||
|
||||||
|
(build === "enterprise" && !isPaid) ||
|
||||||
|
(build === "oss" && !isPaid));
|
||||||
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
resolver: zodResolver(accessControlsFormSchema),
|
resolver: zodResolver(accessControlsFormSchema),
|
||||||
@@ -124,11 +140,28 @@ export default function AccessControlsPage() {
|
|||||||
[roles]
|
[roles]
|
||||||
);
|
);
|
||||||
|
|
||||||
function setRoleTags(
|
function setRoleTags(updater: Tag[] | ((prev: Tag[]) => Tag[])) {
|
||||||
updater: Tag[] | ((prev: Tag[]) => Tag[])
|
|
||||||
) {
|
|
||||||
const prev = form.getValues("roles");
|
const prev = form.getValues("roles");
|
||||||
const next = typeof updater === "function" ? updater(prev) : updater;
|
const nextValue =
|
||||||
|
typeof updater === "function" ? updater(prev) : updater;
|
||||||
|
const next = supportsMultipleRolesPerUser
|
||||||
|
? nextValue
|
||||||
|
: nextValue.length > 1
|
||||||
|
? [nextValue[nextValue.length - 1]]
|
||||||
|
: nextValue;
|
||||||
|
|
||||||
|
// In single-role mode, selecting the currently selected role can transiently
|
||||||
|
// emit an empty tag list from TagInput; keep the prior selection.
|
||||||
|
if (
|
||||||
|
!supportsMultipleRolesPerUser &&
|
||||||
|
next.length === 0 &&
|
||||||
|
prev.length > 0
|
||||||
|
) {
|
||||||
|
form.setValue("roles", [prev[prev.length - 1]], {
|
||||||
|
shouldDirty: true
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (next.length === 0) {
|
if (next.length === 0) {
|
||||||
toast({
|
toast({
|
||||||
@@ -155,11 +188,14 @@ export default function AccessControlsPage() {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const roleIds = values.roles.map((r) => parseInt(r.id, 10));
|
const roleIds = values.roles.map((r) => parseInt(r.id, 10));
|
||||||
|
const updateRoleRequest = supportsMultipleRolesPerUser
|
||||||
|
? api.post(`/user/${user.userId}/org/${orgId}/roles`, {
|
||||||
|
roleIds
|
||||||
|
})
|
||||||
|
: api.post(`/role/${roleIds[0]}/add/${user.userId}`);
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
api.post(`/org/${orgId}/user/${user.userId}/roles`, {
|
updateRoleRequest,
|
||||||
roleIds
|
|
||||||
}),
|
|
||||||
api.post(`/org/${orgId}/user/${user.userId}`, {
|
api.post(`/org/${orgId}/user/${user.userId}`, {
|
||||||
autoProvisioned: values.autoProvisioned
|
autoProvisioned: values.autoProvisioned
|
||||||
})
|
})
|
||||||
@@ -233,7 +269,7 @@ export default function AccessControlsPage() {
|
|||||||
name="roles"
|
name="roles"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="flex flex-col items-start">
|
<FormItem className="flex flex-col items-start">
|
||||||
<FormLabel>{t("role")}</FormLabel>
|
<FormLabel>{t("roles")}</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<TagInput
|
<TagInput
|
||||||
{...field}
|
{...field}
|
||||||
@@ -261,6 +297,17 @@ export default function AccessControlsPage() {
|
|||||||
disabled={loading}
|
disabled={loading}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
{showMultiRolePaywallMessage && (
|
||||||
|
<FormDescription>
|
||||||
|
{build === "saas"
|
||||||
|
? t(
|
||||||
|
"singleRolePerUserPlanNotice"
|
||||||
|
)
|
||||||
|
: t(
|
||||||
|
"singleRolePerUserEditionNotice"
|
||||||
|
)}
|
||||||
|
</FormDescription>
|
||||||
|
)}
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ function getActionsCategories(root: boolean) {
|
|||||||
[t("actionUpdateRole")]: "updateRole",
|
[t("actionUpdateRole")]: "updateRole",
|
||||||
[t("actionListAllowedRoleResources")]: "listRoleResources",
|
[t("actionListAllowedRoleResources")]: "listRoleResources",
|
||||||
[t("actionAddUserRole")]: "addUserRole",
|
[t("actionAddUserRole")]: "addUserRole",
|
||||||
[t("actionSetUserOrgRoles")]: "setUserOrgRoles"
|
[t("actionRemoveUserRole")]: "removeUserRole"
|
||||||
},
|
},
|
||||||
"Access Token": {
|
"Access Token": {
|
||||||
[t("actionGenerateAccessToken")]: "generateAccessToken",
|
[t("actionGenerateAccessToken")]: "generateAccessToken",
|
||||||
|
|||||||
Reference in New Issue
Block a user